Development
This commit is contained in:
parent
a666e1d351
commit
15a4020d12
33 changed files with 1270 additions and 668 deletions
86
api/article/routes.mjs
Normal file
86
api/article/routes.mjs
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { parseFiles } from '../file/util.mjs'
|
||||
import { upload } from '../media/upload.mjs'
|
||||
|
||||
export default class ArticleRoutes {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, {
|
||||
upload: upload,
|
||||
})
|
||||
}
|
||||
|
||||
/** GET: /api/articles/[path] */
|
||||
async getArticle(ctx) {
|
||||
let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path])
|
||||
|
||||
let out = {
|
||||
article: res.results[0][0] || null,
|
||||
files: parseFiles(res.results[1]),
|
||||
}
|
||||
|
||||
ctx.body = out
|
||||
}
|
||||
|
||||
/** GET: /api/auth/articles */
|
||||
async auth_getAllArticles(ctx) {
|
||||
let res = await ctx.db.safeCallProc('article_auth_get_all', [
|
||||
ctx.state.auth_token,
|
||||
Math.max(ctx.query.get('page') || 1, 1),
|
||||
Math.min(ctx.query.get('per_page') || 10, 25)
|
||||
])
|
||||
|
||||
let out = {
|
||||
articles: res.results[0],
|
||||
total_articles: res.results[0][0].total_articles,
|
||||
}
|
||||
|
||||
ctx.body = out
|
||||
}
|
||||
|
||||
|
||||
/** GET: /api/auth/articles/:path */
|
||||
async auth_getSingleArticle(ctx) {
|
||||
let res = await ctx.db.safeCallProc('article_auth_get_update_create', [
|
||||
ctx.state.auth_token,
|
||||
ctx.params.path
|
||||
])
|
||||
|
||||
let out = {
|
||||
article: res.results[0][0] || null,
|
||||
files: parseFiles(res.results[1]),
|
||||
staff: res.results[2],
|
||||
}
|
||||
|
||||
ctx.body = out
|
||||
}
|
||||
|
||||
/** PUT: /api/auth/articles/:path */
|
||||
async auth_updateCreateSingleArticle(ctx) {
|
||||
console.log(ctx.req.files)
|
||||
console.log(ctx.req.body)
|
||||
|
||||
let newBanner = null
|
||||
let newMedia = null
|
||||
|
||||
let promises = []
|
||||
|
||||
if (ctx.req.files.banner) {
|
||||
promises.push(
|
||||
this.upload(ctx.req.files.banner)
|
||||
.then(res => { newBanner = res })
|
||||
)
|
||||
}
|
||||
if (ctx.req.files.media && false) {
|
||||
promises.push(
|
||||
this.upload(ctx.req.files.media)
|
||||
.then(res => { newMedia = res })
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
console.log(newBanner)
|
||||
console.log(newMedia)
|
||||
|
||||
ctx.body = {}
|
||||
}
|
||||
}
|
30
api/authentication/routes.mjs
Normal file
30
api/authentication/routes.mjs
Normal file
|
@ -0,0 +1,30 @@
|
|||
import crypto from 'crypto'
|
||||
import * as util from '../util.mjs'
|
||||
import config from '../config.mjs'
|
||||
|
||||
export default class AuthenticationRoutes {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, {
|
||||
secret: config.get('jwtsecret'),
|
||||
crypto: crypto,
|
||||
util: util,
|
||||
})
|
||||
}
|
||||
|
||||
/** GET: /api/authentication/login */
|
||||
async login(ctx) {
|
||||
let res = await ctx.db.safeCallProc('dbo.common_auth_login', [
|
||||
ctx.req.body.email,
|
||||
ctx.req.body.password,
|
||||
])
|
||||
|
||||
let out = res.results[0][0]
|
||||
|
||||
const hmac = this.crypto.createHmac('sha256', this.secret)
|
||||
hmac.update(out.token)
|
||||
let apiSignature = this.util.encode(hmac.digest())
|
||||
out.token = out.token + '.' + apiSignature
|
||||
|
||||
ctx.body = out
|
||||
}
|
||||
}
|
62
api/authentication/security.mjs
Normal file
62
api/authentication/security.mjs
Normal file
|
@ -0,0 +1,62 @@
|
|||
import crypto from 'crypto'
|
||||
import { HttpError } from 'flaska'
|
||||
import { decode, encode } from '../util.mjs'
|
||||
import config from '../config.mjs'
|
||||
|
||||
const levels = {
|
||||
Normal: 1,
|
||||
Manager: 10,
|
||||
Admin: 100,
|
||||
}
|
||||
|
||||
const issuer = config.get('mssql:connectionUser')
|
||||
const secret = config.get('jwtsecret')
|
||||
|
||||
export function authenticate(minLevel = levels.Manager) {
|
||||
return function(ctx) {
|
||||
if (!ctx.req.headers.authorization) {
|
||||
throw new HttpError(401, 'Authentication token missing')
|
||||
}
|
||||
if (!ctx.req.headers.authorization.startsWith('Bearer ')) {
|
||||
throw new HttpError(401, 'Authentication token invalid')
|
||||
}
|
||||
|
||||
let parts = ctx.req.headers.authorization.slice(7).split('.')
|
||||
|
||||
if (parts.length !== 4) {
|
||||
throw new HttpError(401, 'Authentication token invalid')
|
||||
}
|
||||
|
||||
const hmac = crypto.createHmac('sha256', secret)
|
||||
const token = [parts[0], parts[1], parts[2]].join('.')
|
||||
hmac.update(token)
|
||||
let apiSignature = encode(hmac.digest())
|
||||
|
||||
if (apiSignature !== parts[3]) {
|
||||
throw new HttpError(401, 'Authentication token invalid signature')
|
||||
}
|
||||
|
||||
let header
|
||||
let body
|
||||
try {
|
||||
header = JSON.parse(decode(parts[0]).toString('utf8'))
|
||||
body = JSON.parse(decode(parts[1]).toString('utf8'))
|
||||
} catch (err) {
|
||||
throw new HttpError(401, 'Authentication token invalid json')
|
||||
}
|
||||
|
||||
if (header.alg !== 'HS256') {
|
||||
throw new HttpError(401, 'Authentication token invalid alg')
|
||||
}
|
||||
|
||||
let unixNow = Math.floor(Date.now() / 1000)
|
||||
|
||||
// Validate token, add a little skew support for issued_at
|
||||
if (body.iss !== issuer || !body.iat || !body.exp
|
||||
|| body.iat > unixNow + 300 || body.exp <= unixNow) {
|
||||
throw new HttpError(403, 'Authentication token expired or invalid')
|
||||
}
|
||||
ctx.state.auth_user = body
|
||||
ctx.state.auth_token = token
|
||||
}
|
||||
}
|
109
api/config.mjs
109
api/config.mjs
|
@ -54,6 +54,7 @@ nconf.defaults({
|
|||
"frontend": {
|
||||
"url": "http://beta01.nfp.moe"
|
||||
},
|
||||
"jwtsecret": "w2bkdWAButfdfEkCs8dpE3L2n6QzCfhna0T4",
|
||||
"mssql": {
|
||||
"conn_timeout": 5,
|
||||
"floor": 1,
|
||||
|
@ -62,6 +63,114 @@ nconf.defaults({
|
|||
"inactivityTimeoutSecs": 60,
|
||||
"connectionString": "Driver={ODBC Driver 17 for SQL Server}; Server=localhost;UID=dev; PWD=dev; Database=nfp_moe",
|
||||
},
|
||||
"media": {
|
||||
"secret": "upload-secret-key-here",
|
||||
"iss": "dev",
|
||||
"path": "https://media.nfp.is/media/resize",
|
||||
"preview": {
|
||||
"out": "base64",
|
||||
"format": "avif",
|
||||
"blur": 10,
|
||||
"resize": {
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"fit": "inside",
|
||||
"withoutEnlargement": true,
|
||||
"kernel": "lanczos3"
|
||||
},
|
||||
"avif": {
|
||||
"quality": 50,
|
||||
"effort": 4
|
||||
}
|
||||
},
|
||||
"small": {
|
||||
"jpeg": {
|
||||
"format": "jpeg",
|
||||
"resize": {
|
||||
"width": 720,
|
||||
"height": 720,
|
||||
"fit": "inside",
|
||||
"withoutEnlargement": true,
|
||||
"kernel": "lanczos3"
|
||||
},
|
||||
"jpeg": {
|
||||
"quality": 93
|
||||
}
|
||||
},
|
||||
"avif": {
|
||||
"format": "avif",
|
||||
"resize": {
|
||||
"width": 720,
|
||||
"height": 720,
|
||||
"fit": "inside",
|
||||
"withoutEnlargement": true,
|
||||
"kernel": "lanczos3"
|
||||
},
|
||||
"avif": {
|
||||
"quality": 60,
|
||||
"effort": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"medium": {
|
||||
"jpeg": {
|
||||
"format": "jpeg",
|
||||
"resize": {
|
||||
"width": 1300,
|
||||
"height": 1300,
|
||||
"fit": "inside",
|
||||
"withoutEnlargement": true,
|
||||
"kernel": "lanczos3"
|
||||
},
|
||||
"jpeg": {
|
||||
"quality": 93
|
||||
}
|
||||
},
|
||||
"avif": {
|
||||
"format": "avif",
|
||||
"resize": {
|
||||
"width": 1300,
|
||||
"height": 1300,
|
||||
"fit": "inside",
|
||||
"withoutEnlargement": true,
|
||||
"kernel": "lanczos3"
|
||||
},
|
||||
"avif": {
|
||||
"quality": 75,
|
||||
"effort": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"large": {
|
||||
"jpeg": {
|
||||
"format": "jpeg",
|
||||
"resize": {
|
||||
"width": 3000,
|
||||
"height": 3000,
|
||||
"fit": "inside",
|
||||
"withoutEnlargement": true,
|
||||
"kernel": "lanczos3"
|
||||
},
|
||||
"jpeg": {
|
||||
"quality": 95
|
||||
}
|
||||
},
|
||||
"avif": {
|
||||
"format": "avif",
|
||||
"resize": {
|
||||
"width": 3000,
|
||||
"height": 3000,
|
||||
"fit": "inside",
|
||||
"withoutEnlargement": true,
|
||||
"kernel": "lanczos3"
|
||||
},
|
||||
"avif": {
|
||||
"quality": 85,
|
||||
"effort": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"fileSize": 524288000,
|
||||
"upload": {
|
||||
"baseurl": "https://cdn.nfp.is",
|
||||
|
|
13
api/db.mjs
13
api/db.mjs
|
@ -42,9 +42,18 @@ export function initPool(core, config) {
|
|||
|
||||
return {
|
||||
safeCallProc: function(name, params, options) {
|
||||
return pool.promises.callProc(config.schema + '.' + name, params, options)
|
||||
if (name.indexOf('.') < 0) {
|
||||
name = config.schema + '.' + name
|
||||
}
|
||||
return pool.promises.callProc(name, params, options)
|
||||
.catch(function(err) {
|
||||
let message = err.message
|
||||
let message = err.message.replace(/\[[^\]]+\]/g, '')
|
||||
if (err.code > 50000) {
|
||||
if (err.code === 51001) {
|
||||
throw new HttpError(403, message)
|
||||
}
|
||||
throw new HttpError(422, message)
|
||||
}
|
||||
if (err.lineNumber && err.procName) {
|
||||
message = `Error at ${err.procName}:${err.lineNumber} => ${message}`
|
||||
}
|
||||
|
|
20
api/file/util.mjs
Normal file
20
api/file/util.mjs
Normal file
|
@ -0,0 +1,20 @@
|
|||
export function parseFiles(files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
parseFile(files[i])
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
export function parseFile(file) {
|
||||
file.url = 'https://cdn.nfp.is' + file.path
|
||||
file.magnet = null
|
||||
file.meta = JSON.parse(file.meta || '{}') || {}
|
||||
if (file.meta.torrent) {
|
||||
file.magnet = 'magnet:?'
|
||||
+ 'xl=' + file.size
|
||||
+ '&dn=' + encodeURIComponent(file.meta.torrent.name)
|
||||
+ '&xt=urn:btih:' + file.meta.torrent.hash
|
||||
+ file.meta.torrent.announce.map(item => ('&tr=' + encodeURIComponent(item))).join('')
|
||||
}
|
||||
return file
|
||||
}
|
200
api/media/client.mjs
Normal file
200
api/media/client.mjs
Normal file
|
@ -0,0 +1,200 @@
|
|||
import crypto from 'crypto'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import http from 'http'
|
||||
import https from 'https'
|
||||
import { URL } from 'url'
|
||||
import { encode } from '../util.mjs'
|
||||
|
||||
export default class Client {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
customRequest(method = 'GET', path, body, options = {}) {
|
||||
if (path.slice(0, 4) !== 'http') {
|
||||
throw new Error('Http client path invalid')
|
||||
}
|
||||
let urlObj = new URL(path)
|
||||
let d1 = new Date()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = {
|
||||
method: options.method || method,
|
||||
timeout: options.timeout || 60000,
|
||||
protocol: options.protocol || urlObj.protocol,
|
||||
username: options.username || urlObj.username,
|
||||
password: options.password || urlObj.password,
|
||||
host: options.host || urlObj.hostname,
|
||||
port: options.port || Number(urlObj.port),
|
||||
path: options.path || urlObj.pathname + urlObj.search,
|
||||
headers: options.headers || {},
|
||||
}
|
||||
|
||||
if (options.agent) {
|
||||
opts.agent = options.agent
|
||||
}
|
||||
|
||||
// opts.agent = agent
|
||||
|
||||
let req
|
||||
if (path.startsWith('https')) {
|
||||
req = https.request(opts)
|
||||
} else {
|
||||
req = http.request(opts)
|
||||
}
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject(err)
|
||||
})
|
||||
req.on('timeout', function() {
|
||||
console.log("req.on('timeout')")
|
||||
req.destroy()
|
||||
let d2 = new Date()
|
||||
console.log((d2 - d1))
|
||||
reject(new Error(`Request ${method} ${path} timed out`))
|
||||
})
|
||||
req.on('response', res => {
|
||||
let output = ''
|
||||
res.setEncoding('utf8')
|
||||
|
||||
res.on('data', function (chunk) {
|
||||
output += chunk.toString()
|
||||
})
|
||||
|
||||
res.on('end', function () {
|
||||
if (options.getRaw) {
|
||||
output = {
|
||||
status: res.statusCode,
|
||||
data: output,
|
||||
headers: res.headers,
|
||||
}
|
||||
} else {
|
||||
if (!output) return resolve(null)
|
||||
try {
|
||||
output = JSON.parse(output)
|
||||
} catch (e) {
|
||||
return reject(new Error(`${e.message} while decoding: ${output}`))
|
||||
}
|
||||
}
|
||||
if (!options.getRaw && output.status && typeof(output.status) === 'number') {
|
||||
let err = new Error(`Request failed [${output.status}]: ${output.message}`)
|
||||
err.body = output
|
||||
return reject(err)
|
||||
}
|
||||
resolve(output)
|
||||
})
|
||||
})
|
||||
|
||||
if (opts.returnRequest) {
|
||||
return resolve(req)
|
||||
}
|
||||
|
||||
if (body) {
|
||||
req.write(body)
|
||||
}
|
||||
req.end()
|
||||
return req
|
||||
})
|
||||
}
|
||||
|
||||
createJwt(body, secret) {
|
||||
let header = {
|
||||
typ: 'JWT',
|
||||
alg: 'HS256',
|
||||
}
|
||||
|
||||
body.iat = Math.floor(Date.now() / 1000)
|
||||
body.exp = body.iat + 300
|
||||
|
||||
// Base64 encode header and body
|
||||
let headerBase64 = encode(Buffer.from(JSON.stringify(header)))
|
||||
let bodyBase64 = encode(Buffer.from(JSON.stringify(body)))
|
||||
let headerBodyBase64 = headerBase64 + '.' + bodyBase64
|
||||
|
||||
const hmac = crypto.createHmac('sha256', secret)
|
||||
hmac.update(headerBodyBase64)
|
||||
let signatureBuffer = hmac.digest()
|
||||
|
||||
// Construct final JWT
|
||||
let signatureBase64 = encode(signatureBuffer)
|
||||
return headerBodyBase64 + '.' + signatureBase64
|
||||
}
|
||||
|
||||
random(length = 8) {
|
||||
// Declare all characters
|
||||
let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
// Pick characers randomly
|
||||
let str = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
str += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
post(url, body) {
|
||||
let parsed = JSON.stringify(body)
|
||||
return this.customRequest('POST', url, parsed, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': parsed.length,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
upload(url, files, method = 'POST', body = {}) {
|
||||
const boundary = `---------${this.random(32)}`
|
||||
const crlf = '\r\n'
|
||||
let upload = files
|
||||
|
||||
if (typeof(upload) === 'string') {
|
||||
upload = {
|
||||
file: files
|
||||
}
|
||||
}
|
||||
let keys = Object.keys(upload)
|
||||
let uploadBody = []
|
||||
|
||||
return Promise.all(keys.map(key => {
|
||||
let file = upload[key]
|
||||
let filename = ''
|
||||
if (typeof(file) === 'object') {
|
||||
if (typeof(file.file) !== 'string' || typeof(file.filename) !== 'string') {
|
||||
throw new Error('Invalid value in client.upload for key ' + key)
|
||||
}
|
||||
filename = file.filename
|
||||
file = file.file
|
||||
} else {
|
||||
filename = path.basename(file)
|
||||
}
|
||||
return fs.readFile(file).then(data => {
|
||||
uploadBody.push(Buffer.from(
|
||||
`${crlf}--${boundary}${crlf}` +
|
||||
`Content-Disposition: form-data; name="${key}"; filename="${filename}"` + crlf + crlf
|
||||
))
|
||||
uploadBody.push(data)
|
||||
})
|
||||
}))
|
||||
.then(() => {
|
||||
uploadBody.push(
|
||||
Buffer.concat(Object.keys(body).map(function(key) {
|
||||
return Buffer.from(''
|
||||
+ `${crlf}--${boundary}${crlf}`
|
||||
+ `Content-Disposition: form-data; name="${key}"` + crlf + crlf
|
||||
+ JSON.stringify(body[key])
|
||||
)
|
||||
}))
|
||||
)
|
||||
uploadBody.push(Buffer.from(`${crlf}--${boundary}--`))
|
||||
|
||||
let multipartBody = Buffer.concat(uploadBody)
|
||||
|
||||
return this.customRequest(method, url, multipartBody, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data; boundary=' + boundary,
|
||||
'Content-Length': multipartBody.length,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
51
api/media/upload.mjs
Normal file
51
api/media/upload.mjs
Normal file
|
@ -0,0 +1,51 @@
|
|||
import config from '../config.mjs'
|
||||
import Client from './client.mjs'
|
||||
|
||||
export function upload(file) {
|
||||
const media = config.get('media')
|
||||
|
||||
const client = new Client()
|
||||
let token = client.createJwt({ iss: media.iss }, media.secret)
|
||||
|
||||
let out = {
|
||||
small: {},
|
||||
medium: {},
|
||||
large: {},
|
||||
}
|
||||
|
||||
return client.upload(media.path + '?token=' + token, { file: {
|
||||
file: file.path,
|
||||
filename: file.name,
|
||||
} }, 'POST', {
|
||||
preview: media.preview,
|
||||
small: media.small.avif,
|
||||
medium: media.medium.avif,
|
||||
large: media.large.avif,
|
||||
/*
|
||||
|
||||
small: media.small.jpeg,
|
||||
medium: media.medium.avif,
|
||||
large_jpeg: media.large.jpeg,*/
|
||||
}).then(res => {
|
||||
out.filename = res.filename
|
||||
out.path = res.path
|
||||
out.preview = res.preview
|
||||
out.small.avif = res.small
|
||||
out.medium.avif = res.medium
|
||||
out.large.avif = res.large
|
||||
|
||||
return client.post(media.path + '/' + out.filename + '?token=' + token, {
|
||||
small: media.small.jpeg,
|
||||
medium: media.medium.jpeg,
|
||||
large: media.large.jpeg,
|
||||
})
|
||||
.then(res => {
|
||||
out.small.jpeg = res.small
|
||||
out.medium.jpeg = res.medium
|
||||
out.large.jpeg = res.large
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
return out
|
||||
})
|
||||
}
|
|
@ -1,26 +1,7 @@
|
|||
/*
|
||||
|
||||
Page model:
|
||||
{
|
||||
filename,
|
||||
filetype,
|
||||
small_image,
|
||||
medium_image,
|
||||
large_image,
|
||||
*small_url,
|
||||
*medium_url,
|
||||
*large_url,
|
||||
size,
|
||||
staff_id,
|
||||
is_deleted,
|
||||
created_at,
|
||||
updated_at,
|
||||
}
|
||||
|
||||
*/
|
||||
import { parseFile } from '../file/util.mjs'
|
||||
|
||||
export async function getTree(ctx) {
|
||||
let res = await ctx.db.safeCallProc('pages_gettree', [])
|
||||
let res = await ctx.db.safeCallProc('pages_get_tree', [])
|
||||
let out = []
|
||||
let children = []
|
||||
let map = new Map()
|
||||
|
@ -45,7 +26,22 @@ export async function getTree(ctx) {
|
|||
}
|
||||
|
||||
export async function getPage(ctx, path, page = 0, per_page = 10) {
|
||||
let res = await ctx.db.safeCallProc('pages_getpage', [path, page, per_page])
|
||||
console.log([path, page, per_page])
|
||||
console.log(res.results)
|
||||
let res = await ctx.db.safeCallProc('pages_get_single', [path, page, per_page])
|
||||
|
||||
let articleMap = new Map()
|
||||
|
||||
let out = {
|
||||
page: res.results[0][0] || null,
|
||||
articles: res.results[1],
|
||||
total_articles: res.results[2][0].total_articles,
|
||||
featured: res.results[4][0] || null
|
||||
}
|
||||
out.articles.forEach(article => {
|
||||
article.files = []
|
||||
articleMap.set(article.id, article)
|
||||
})
|
||||
res.results[3].forEach(file => {
|
||||
articleMap.get(file.id).files.push(parseFile(file))
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
|
|
@ -14,13 +14,13 @@ export default class PageRoutes {
|
|||
ctx.body = await this.Page.getTree(ctx)
|
||||
}
|
||||
|
||||
/** GET: /api/page/[path] */
|
||||
/** GET: /api/pages/[path] */
|
||||
async getPage(ctx) {
|
||||
ctx.body = await this.Page.getPage(
|
||||
ctx,
|
||||
ctx.params.path || null,
|
||||
ctx.query.get('page') || 1,
|
||||
ctx.query.get('per_page') || 10
|
||||
Math.max(ctx.query.get('page') || 1, 1),
|
||||
Math.min(ctx.query.get('per_page') || 10, 25)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import { Flaska, QueryHandler } from 'flaska'
|
||||
import { Flaska, QueryHandler, JsonHandler, FormidableHandler } from 'flaska'
|
||||
import formidable from 'formidable'
|
||||
|
||||
import { initPool } from './db.mjs'
|
||||
import config from './config.mjs'
|
||||
import PageRoutes from './page/routes.mjs'
|
||||
import ServeHandler from './serve.mjs'
|
||||
// import ArticleRoutes from './article/routes.mjs'
|
||||
import ParserMiddleware from './pagination/parser.mjs'
|
||||
import ArticleRoutes from './article/routes.mjs'
|
||||
import AuthenticationRoutes from './authentication/routes.mjs'
|
||||
import { authenticate } from './authentication/security.mjs'
|
||||
|
||||
export function run(http, port, core) {
|
||||
let localUtil = new core.sc.Util(import.meta.url)
|
||||
|
||||
// Create our server
|
||||
const flaska = new Flaska({
|
||||
appendHeaders: {
|
||||
'Content-Security-Policy': `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`,
|
||||
},
|
||||
log: core.log,
|
||||
nonce: ['script-src'],
|
||||
nonceCacheLength: 50,
|
||||
|
@ -25,16 +30,12 @@ export function run(http, port, core) {
|
|||
flaska.devMode()
|
||||
}
|
||||
|
||||
const parser = new ParserMiddleware()
|
||||
|
||||
flaska.before(function(ctx) {
|
||||
ctx.state.started = new Date().getTime()
|
||||
ctx.db = pool
|
||||
})
|
||||
flaska.before(QueryHandler())
|
||||
flaska.before(parser.contextParser())
|
||||
|
||||
//
|
||||
flaska.after(function(ctx) {
|
||||
let ended = new Date().getTime()
|
||||
var requestTime = ended - ctx.state.started
|
||||
|
@ -61,11 +62,16 @@ export function run(http, port, core) {
|
|||
flaska.get('/api/pages/:path', page.getPage.bind(page))
|
||||
// flaska.get('/api/pages/:pageId', page.getSinglePage.bind(page))
|
||||
|
||||
// const article = new ArticleRoutes()
|
||||
// flaska.get('/api/articles/public', article.getPublicAllArticles.bind(article))
|
||||
// flaska.get('/api/articles/public/:id', article.getPublicSingleArticle.bind(article))
|
||||
const article = new ArticleRoutes()
|
||||
flaska.get('/api/articles/:path', article.getArticle.bind(article))
|
||||
flaska.get('/api/auth/articles', authenticate(), article.auth_getAllArticles.bind(article))
|
||||
flaska.get('/api/auth/articles/:path', authenticate(), article.auth_getSingleArticle.bind(article))
|
||||
flaska.put('/api/auth/articles/:path', [authenticate(), FormidableHandler(formidable) ], article.auth_updateCreateSingleArticle.bind(article))
|
||||
// flaska.get('/api/pages/:pageId/articles/public', article.getPublicAllPageArticles.bind(article))
|
||||
|
||||
const authentication = new AuthenticationRoutes()
|
||||
flaska.post('/api/authentication/login', JsonHandler(), authentication.login.bind(authentication))
|
||||
|
||||
const serve = new ServeHandler({
|
||||
root: localUtil.getPathFromRoot('../public'),
|
||||
version: core.app.running,
|
||||
|
|
21
api/util.mjs
Normal file
21
api/util.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
export function encode(buffer) {
|
||||
return buffer
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-') // Convert '+' to '-'
|
||||
.replace(/\//g, '_') // Convert '/' to '_'
|
||||
.replace(/=+$/, '') // Remove ending '='
|
||||
}
|
||||
|
||||
export function decode(base64StringUrlSafe) {
|
||||
let base64String = base64StringUrlSafe.replace(/-/g, '+').replace(/_/g, '/')
|
||||
switch (base64String.length % 4) {
|
||||
case 2:
|
||||
base64String += '=='
|
||||
break
|
||||
case 3:
|
||||
base64String += '='
|
||||
break
|
||||
}
|
||||
return Buffer.from(base64String, 'base64')
|
||||
}
|
|
@ -2,44 +2,71 @@ const Article = require('../api/article')
|
|||
const pagination = require('../api/pagination')
|
||||
const Dialogue = require('../widgets/dialogue')
|
||||
const Pages = require('../widgets/pages')
|
||||
const common = require('../api/common')
|
||||
|
||||
const AdminArticles = {
|
||||
oninit: function(vnode) {
|
||||
this.error = ''
|
||||
this.lastpage = m.route.param('page') || '1'
|
||||
this.articles = []
|
||||
this.loading = false
|
||||
this.showLoading = null
|
||||
this.data = {
|
||||
articles: [],
|
||||
total_articles: 0,
|
||||
}
|
||||
this.removeArticle = null
|
||||
this.currentPage = Number(m.route.param('page')) || 1
|
||||
|
||||
this.fetchArticles(vnode)
|
||||
},
|
||||
|
||||
onupdate: function(vnode) {
|
||||
if (m.route.param('page') && m.route.param('page') !== this.lastpage) {
|
||||
onbeforeupdate: function(vnode) {
|
||||
this.currentPage = Number(m.route.param('page')) || 1
|
||||
|
||||
if (this.currentPage !== this.lastpage) {
|
||||
this.fetchArticles(vnode)
|
||||
}
|
||||
},
|
||||
|
||||
fetchArticles: function(vnode) {
|
||||
this.loading = true
|
||||
this.links = null
|
||||
this.lastpage = m.route.param('page') || '1'
|
||||
this.error = ''
|
||||
this.lastpage = this.currentPage
|
||||
|
||||
document.title = 'Articles Page ' + this.lastpage + ' - Admin NFP Moe'
|
||||
|
||||
return pagination.fetchPage(Article.getAllArticlesPagination({
|
||||
per_page: 20,
|
||||
page: this.lastpage,
|
||||
includes: ['parent', 'staff'],
|
||||
}))
|
||||
.then(function(result) {
|
||||
vnode.state.articles = result.data
|
||||
vnode.state.links = result.links
|
||||
if (this.showLoading) {
|
||||
clearTimeout(this.showLoading)
|
||||
}
|
||||
|
||||
if (this.data.articles.length) {
|
||||
this.showLoading = setTimeout(() => {
|
||||
this.showLoading = null
|
||||
this.loading = true
|
||||
m.redraw()
|
||||
}, 150)
|
||||
} else {
|
||||
this.loading = true
|
||||
}
|
||||
|
||||
return common.sendRequest({
|
||||
method: 'GET',
|
||||
url: '/api/auth/articles?page=' + (this.lastpage || 1),
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
.then((result) => {
|
||||
console.log(result)
|
||||
this.data = result
|
||||
|
||||
this.data.articles.forEach((article) => {
|
||||
article.hidden = new Date() < new Date(article.publish_at)
|
||||
article.page_path = article.page_path ? '/page/' + article.page_path : '/'
|
||||
article.page_name = article.page_name || 'Frontpage'
|
||||
})
|
||||
.then(function() {
|
||||
vnode.state.loading = false
|
||||
}, (err) => {
|
||||
this.error = err.message
|
||||
})
|
||||
.then(() => {
|
||||
clearTimeout(this.showLoading)
|
||||
this.showLoading = null
|
||||
this.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
@ -58,31 +85,19 @@ const AdminArticles = {
|
|||
},
|
||||
|
||||
drawArticle: function(vnode, article) {
|
||||
let parent
|
||||
if (article.parent) {
|
||||
parent = {
|
||||
path: '/page/' + article.parent.path,
|
||||
name: article.parent.name,
|
||||
}
|
||||
} else {
|
||||
parent = {
|
||||
path: '/',
|
||||
name: '-- Frontpage --',
|
||||
}
|
||||
}
|
||||
let className = ''
|
||||
if (new Date() < new Date(article.published_at)) {
|
||||
className = 'rowhidden'
|
||||
} else if (article.is_featured) {
|
||||
className = 'rowfeatured'
|
||||
}
|
||||
return [
|
||||
m('tr', { class: className }, [
|
||||
m('td', m(m.route.Link, { href: '/admin/articles/' + article.id }, article.name)),
|
||||
m('td', m(m.route.Link, { href: parent.path }, parent.name)),
|
||||
m('tr', {
|
||||
class: article.hidden
|
||||
? rowhidden
|
||||
: article.is_featured
|
||||
? 'rowfeatured'
|
||||
: ''
|
||||
}, [
|
||||
m('td', m(m.route.Link, { href: '/admin/articles/' + article.path }, article.name)),
|
||||
m('td', m(m.route.Link, { href: article.page_path }, article.page_name)),
|
||||
m('td', m(m.route.Link, { href: '/article/' + article.path }, '/article/' + article.path)),
|
||||
m('td.right', article.published_at.replace('T', ' ').split('.')[0]),
|
||||
m('td.right', article.staff && article.staff.fullname || 'Admin'),
|
||||
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')),
|
||||
]),
|
||||
]
|
||||
|
@ -101,7 +116,7 @@ const AdminArticles = {
|
|||
hidden: !this.error,
|
||||
onclick: function() { vnode.state.error = '' },
|
||||
}, this.error),
|
||||
(this.loading
|
||||
this.loading
|
||||
? m('div.loading-spinner.full')
|
||||
: m('table', [
|
||||
m('thead',
|
||||
|
@ -114,13 +129,13 @@ const AdminArticles = {
|
|||
m('th.right', 'Actions'),
|
||||
])
|
||||
),
|
||||
m('tbody', this.articles.map(AdminArticles.drawArticle.bind(this, vnode))),
|
||||
])
|
||||
m('tbody', this.data.articles.map((article) => this.drawArticle(vnode, article))),
|
||||
],
|
||||
),
|
||||
m(Pages, {
|
||||
/*m(Pages, {
|
||||
base: '/admin/articles',
|
||||
links: this.links,
|
||||
}),
|
||||
}),*/
|
||||
]),
|
||||
]),
|
||||
m(Dialogue, {
|
||||
|
|
|
@ -6,189 +6,199 @@ const Page = require('../api/page')
|
|||
const File = require('../api/file')
|
||||
const Fileinfo = require('../widgets/fileinfo')
|
||||
const Article = require('../api/article')
|
||||
const common = require('../api/common')
|
||||
|
||||
const EditArticle = {
|
||||
getFroalaOptions: function() {
|
||||
return {
|
||||
theme: 'gray',
|
||||
heightMin: 150,
|
||||
videoUpload: false,
|
||||
imageUploadURL: '/api/media',
|
||||
imageManagerLoadURL: '/api/media',
|
||||
imageManagerDeleteMethod: 'DELETE',
|
||||
imageManagerDeleteURL: '/api/media',
|
||||
events: {
|
||||
'imageManager.beforeDeleteImage': function(img) {
|
||||
this.opts.imageManagerDeleteURL = '/api/media/' + img.data('id')
|
||||
},
|
||||
},
|
||||
requestHeaders: {
|
||||
'Authorization': 'Bearer ' + Authentication.getToken(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
oninit: function(vnode) {
|
||||
this.froala = null
|
||||
this.loadedFroala = Froala.loadedFroala
|
||||
this.staffers = []
|
||||
this.editor = null
|
||||
|
||||
Staff.getAllStaff()
|
||||
.then(function(result) {
|
||||
vnode.state.staffers = result
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
if (!this.loadedFroala) {
|
||||
Froala.createFroalaScript()
|
||||
.then(function() {
|
||||
vnode.state.loadedFroala = true
|
||||
m.redraw()
|
||||
})
|
||||
this.loading = false
|
||||
this.showLoading = null
|
||||
this.data = {
|
||||
article: null,
|
||||
files: [],
|
||||
staff: [],
|
||||
}
|
||||
this.pages = [{id: null, name: 'Frontpage'}]
|
||||
this.addPageTree('', Page.Tree)
|
||||
this.newBanner = null
|
||||
this.newMedia = null
|
||||
|
||||
this.fetchArticle(vnode)
|
||||
},
|
||||
|
||||
onupdate: function(vnode) {
|
||||
addPageTree: function(prefix, branches) {
|
||||
branches.forEach((page) => {
|
||||
this.pages.push({ id: page.id, name: prefix + page.name })
|
||||
if (page.children && page.children.length) {
|
||||
this.addPageTree(page.name + ' -> ', page.children)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onbeforeupdate: function(vnode) {
|
||||
if (this.lastid !== m.route.param('id')) {
|
||||
this.fetchArticle(vnode)
|
||||
if (this.lastid === 'add') {
|
||||
m.redraw()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
fetchArticle: function(vnode) {
|
||||
this.lastid = m.route.param('id')
|
||||
this.loading = this.lastid !== 'add'
|
||||
this.creating = this.lastid === 'add'
|
||||
this.loadingFile = false
|
||||
this.error = ''
|
||||
this.article = {
|
||||
name: '',
|
||||
path: '',
|
||||
description: '',
|
||||
media: null,
|
||||
banner: null,
|
||||
files: [],
|
||||
is_featured: false,
|
||||
published_at: new Date(new Date().setFullYear(3000)).toISOString(),
|
||||
}
|
||||
this.editedPath = false
|
||||
this.loadedFroala = Froala.loadedFroala
|
||||
|
||||
if (this.lastid !== 'add') {
|
||||
Article.getArticle(this.lastid)
|
||||
.then(function(result) {
|
||||
vnode.state.editedPath = true
|
||||
vnode.state.article = result
|
||||
EditArticle.parsePublishedAt(vnode, null)
|
||||
document.title = 'Editing: ' + result.name + ' - Admin NFP Moe'
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
vnode.state.loading = false
|
||||
if (vnode.state.froala) {
|
||||
vnode.state.froala.html.set(vnode.state.article.description)
|
||||
if (this.showLoading) {
|
||||
clearTimeout(this.showLoading)
|
||||
}
|
||||
|
||||
if (this.data.article) {
|
||||
this.showLoading = setTimeout(() => {
|
||||
this.showLoading = null
|
||||
this.loading = true
|
||||
m.redraw()
|
||||
}, 150)
|
||||
} else {
|
||||
this.loading = true
|
||||
}
|
||||
|
||||
return common.sendRequest({
|
||||
method: 'GET',
|
||||
url: '/api/auth/articles/' + this.lastid,
|
||||
})
|
||||
.then((result) => {
|
||||
console.log('result', result)
|
||||
this.data = result
|
||||
if (this.data.article) {
|
||||
this.data.article.publish_at = new Date(this.data.article.publish_at)
|
||||
document.title = 'Editing: ' + this.data.article.name + ' - Admin NFP Moe'
|
||||
} else {
|
||||
document.title = 'Create Article - Admin NFP Moe'
|
||||
}
|
||||
}, (err) => {
|
||||
this.error = err.message
|
||||
})
|
||||
.then(() => {
|
||||
clearTimeout(this.showLoading)
|
||||
this.showLoading = null
|
||||
this.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
} else {
|
||||
EditArticle.parsePublishedAt(vnode, null)
|
||||
document.title = 'Create Article - Admin NFP Moe'
|
||||
if (vnode.state.froala) {
|
||||
vnode.state.froala.html.set(this.article.description)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
parsePublishedAt: function(vnode, date) {
|
||||
vnode.state.article.published_at = ((date && date.toISOString() || vnode.state.article.published_at).split('.')[0]).substr(0, 16)
|
||||
},
|
||||
|
||||
updateValue: function(name, e) {
|
||||
if (name === 'is_featured') {
|
||||
this.article[name] = e.currentTarget.checked
|
||||
this.data.article[name] = e.currentTarget.checked
|
||||
} else {
|
||||
this.article[name] = e.currentTarget.value
|
||||
this.data.article[name] = e.currentTarget.value
|
||||
}
|
||||
if (name === 'path') {
|
||||
this.editedPath = true
|
||||
} else if (name === 'name' && !this.editedPath) {
|
||||
this.article.path = this.article.name.toLowerCase().replace(/ /g, '-')
|
||||
this.data.article.path = this.data.article.name.toLowerCase().replace(/ /g, '-')
|
||||
}
|
||||
},
|
||||
|
||||
updateParent: function(e) {
|
||||
this.article.parent_id = Number(e.currentTarget.value)
|
||||
if (this.article.parent_id === -1) {
|
||||
this.article.parent_id = null
|
||||
}
|
||||
this.data.article.page_id = Number(e.currentTarget.value) || null
|
||||
},
|
||||
|
||||
updateStaffer: function(e) {
|
||||
this.article.staff_id = Number(e.currentTarget.value)
|
||||
this.data.article.admin_id = Number(e.currentTarget.value)
|
||||
},
|
||||
|
||||
mediaUploaded: function(type, media) {
|
||||
this.article[type] = media
|
||||
mediaUploaded: function(type, file) {
|
||||
if (type === 'banner') {
|
||||
this.newBanner = file
|
||||
} else {
|
||||
this.newMedia = file
|
||||
}
|
||||
},
|
||||
|
||||
mediaRemoved: function(type) {
|
||||
this.article[type] = null
|
||||
this.data.article[type] = null
|
||||
},
|
||||
|
||||
save: function(vnode, e) {
|
||||
e.preventDefault()
|
||||
if (!this.article.name) {
|
||||
console.log(this.data)
|
||||
|
||||
let formData = new FormData()
|
||||
if (this.newBanner) {
|
||||
formData.append('banner', this.newBanner.file)
|
||||
}
|
||||
if (this.newMedia) {
|
||||
formData.append('media', this.newMedia.file)
|
||||
}
|
||||
if (this.data.article.id) {
|
||||
formData.append('id', this.data.article.id)
|
||||
}
|
||||
|
||||
formData.append('admin_id', this.data.article.admin_id)
|
||||
formData.append('name', this.data.article.name)
|
||||
formData.append('content', this.data.article.content)
|
||||
formData.append('is_featured', this.data.article.is_featured)
|
||||
formData.append('path', this.data.article.path)
|
||||
formData.append('page_id', this.data.article.page_id)
|
||||
formData.append('publish_at', this.data.article.publish_at)
|
||||
|
||||
this.loading = true
|
||||
|
||||
common.sendRequest({
|
||||
method: 'PUT',
|
||||
url: '/api/auth/articles/' + this.lastid,
|
||||
body: formData,
|
||||
})
|
||||
.then((result) => {
|
||||
console.log('result', result)
|
||||
}, (err) => {
|
||||
this.error = err.message
|
||||
})
|
||||
.then(() => {
|
||||
this.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
/*e.preventDefault()
|
||||
if (!this.data.article.name) {
|
||||
this.error = 'Name is missing'
|
||||
} else if (!this.article.path) {
|
||||
} else if (!this.data.article.path) {
|
||||
this.error = 'Path is missing'
|
||||
} else {
|
||||
this.error = ''
|
||||
}
|
||||
if (this.error) return
|
||||
|
||||
this.article.description = vnode.state.froala && vnode.state.froala.html.get() || this.article.description
|
||||
if (this.article.description) {
|
||||
this.article.description = this.article.description.replace(/<p[^>]+data-f-id="pbf"[^>]+>[^>]+>[^>]+>[^>]+>/, '')
|
||||
this.data.article.description = vnode.state.froala && vnode.state.froala.html.get() || this.data.article.description
|
||||
if (this.data.article.description) {
|
||||
this.data.article.description = this.data.article.description.replace(/<p[^>]+data-f-id="pbf"[^>]+>[^>]+>[^>]+>[^>]+>/, '')
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
let promise
|
||||
|
||||
if (this.article.id) {
|
||||
promise = Article.updateArticle(this.article.id, {
|
||||
name: this.article.name,
|
||||
path: this.article.path,
|
||||
parent_id: this.article.parent_id,
|
||||
description: this.article.description,
|
||||
banner_id: this.article.banner && this.article.banner.id,
|
||||
media_id: this.article.media && this.article.media.id,
|
||||
published_at: new Date(this.article.published_at),
|
||||
is_featured: this.article.is_featured,
|
||||
staff_id: this.article.staff_id,
|
||||
if (this.data.article.id) {
|
||||
promise = Article.updateArticle(this.data.article.id, {
|
||||
name: this.data.article.name,
|
||||
path: this.data.article.path,
|
||||
page_id: this.data.article.page_id,
|
||||
description: this.data.article.description,
|
||||
banner_id: this.data.article.banner && this.data.article.banner.id,
|
||||
media_id: this.data.article.media && this.data.article.media.id,
|
||||
publish_at: new Date(this.data.article.publish_at),
|
||||
is_featured: this.data.article.is_featured,
|
||||
staff_id: this.data.article.staff_id,
|
||||
})
|
||||
} else {
|
||||
promise = Article.createArticle({
|
||||
name: this.article.name,
|
||||
path: this.article.path,
|
||||
parent_id: this.article.parent_id,
|
||||
description: this.article.description,
|
||||
banner_id: this.article.banner && this.article.banner.id,
|
||||
media_id: this.article.media && this.article.media.id,
|
||||
published_at: new Date(this.article.published_at),
|
||||
is_featured: this.article.is_featured,
|
||||
staff_id: this.article.staff_id,
|
||||
name: this.data.article.name,
|
||||
path: this.data.article.path,
|
||||
page_id: this.data.article.page_id,
|
||||
description: this.data.article.description,
|
||||
banner_id: this.data.article.banner && this.data.article.banner.id,
|
||||
media_id: this.data.article.media && this.data.article.media.id,
|
||||
publish_at: new Date(this.data.article.publish_at),
|
||||
is_featured: this.data.article.is_featured,
|
||||
staff_id: this.data.article.staff_id,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -209,86 +219,48 @@ const EditArticle = {
|
|||
.then(function() {
|
||||
vnode.state.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
})*/
|
||||
},
|
||||
|
||||
uploadFile: function(vnode, event) {
|
||||
if (!event.target.files[0]) return
|
||||
vnode.state.error = ''
|
||||
vnode.state.loadingFile = true
|
||||
uploadFile: function(vnode, e) {
|
||||
|
||||
File.uploadFile(this.article.id, event.target.files[0])
|
||||
.then(function(res) {
|
||||
vnode.state.article.files.push(res)
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
event.target.value = null
|
||||
vnode.state.loadingFile = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
getFlatTree: function() {
|
||||
let out = [{id: null, name: '-- Frontpage --'}]
|
||||
Page.Tree.forEach(function(page) {
|
||||
out.push({ id: page.id, name: page.name })
|
||||
if (page.children.length) {
|
||||
page.children.forEach(function(sub) {
|
||||
out.push({ id: sub.id, name: page.name + ' -> ' + sub.name })
|
||||
})
|
||||
}
|
||||
})
|
||||
return out
|
||||
},
|
||||
|
||||
getStaffers: function() {
|
||||
if (!this.article.staff_id) {
|
||||
this.article.staff_id = 1
|
||||
}
|
||||
let out = []
|
||||
this.staffers.forEach(function(item) {
|
||||
out.push({ id: item.id, name: item.fullname })
|
||||
})
|
||||
return out
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
const showPublish = new Date(this.article.published_at) > new Date()
|
||||
const parents = this.getFlatTree()
|
||||
const staffers = this.getStaffers()
|
||||
return (
|
||||
const showPublish = this.data.article
|
||||
? this.data.article.publish_at > new Date()
|
||||
: false
|
||||
|
||||
return [
|
||||
this.loading ?
|
||||
m('div.loading-spinner')
|
||||
: m('div.admin-wrapper', [
|
||||
m('div.admin-actions', this.article.id
|
||||
: null,
|
||||
this.data.article
|
||||
? m('div.admin-wrapper', [
|
||||
m('div.admin-actions', this.data.article.id
|
||||
? [
|
||||
m('span', 'Actions:'),
|
||||
m(m.route.Link, { href: '/article/' + this.article.path }, 'View article'),
|
||||
m(m.route.Link, { href: '/article/' + this.data.article.path }, 'View article'),
|
||||
]
|
||||
: null),
|
||||
m('article.editarticle', [
|
||||
m('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (this.article.name || '(untitled)'))),
|
||||
m('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (this.data.article.name || '(untitled)'))),
|
||||
m('div.error', {
|
||||
hidden: !this.error,
|
||||
onclick: function() { vnode.state.error = '' },
|
||||
onclick: () => { vnode.state.error = '' },
|
||||
}, this.error),
|
||||
m(FileUpload, {
|
||||
height: 300,
|
||||
onupload: this.mediaUploaded.bind(this, 'banner'),
|
||||
onerror: function(e) { vnode.state.error = e },
|
||||
onfile: this.mediaUploaded.bind(this, 'banner'),
|
||||
ondelete: this.mediaRemoved.bind(this, 'banner'),
|
||||
media: this.article && this.article.banner,
|
||||
media: this.data.article && this.data.article.banner,
|
||||
}),
|
||||
m(FileUpload, {
|
||||
class: 'cover',
|
||||
useimg: true,
|
||||
onupload: this.mediaUploaded.bind(this, 'media'),
|
||||
onfile: this.mediaUploaded.bind(this, 'media'),
|
||||
ondelete: this.mediaRemoved.bind(this, 'media'),
|
||||
onerror: function(e) { vnode.state.error = e },
|
||||
media: this.article && this.article.media,
|
||||
media: this.data.article && this.data.article.media,
|
||||
}),
|
||||
m('form.editarticle.content', {
|
||||
onsubmit: this.save.bind(this, vnode),
|
||||
|
@ -296,27 +268,30 @@ const EditArticle = {
|
|||
m('label', 'Parent'),
|
||||
m('select', {
|
||||
onchange: this.updateParent.bind(this),
|
||||
}, parents.map(function(item) { return m('option', { value: item.id || -1, selected: item.id === vnode.state.article.parent_id }, item.name) })),
|
||||
}, this.pages.map((item) => {
|
||||
return m('option', {
|
||||
value: item.id || 0,
|
||||
selected: item.id === this.data.article.page_id
|
||||
}, item.name)
|
||||
})),
|
||||
m('label', 'Name'),
|
||||
m('input', {
|
||||
type: 'text',
|
||||
value: this.article.name,
|
||||
value: this.data.article.name,
|
||||
oninput: this.updateValue.bind(this, 'name'),
|
||||
}),
|
||||
m('label.slim', 'Path'),
|
||||
m('input.slim', {
|
||||
type: 'text',
|
||||
value: this.article.path,
|
||||
value: this.data.article.path,
|
||||
oninput: this.updateValue.bind(this, 'path'),
|
||||
}),
|
||||
m('label', 'Description'),
|
||||
(
|
||||
this.loadedFroala ?
|
||||
m('div', {
|
||||
oncreate: function(div) {
|
||||
vnode.state.froala = new FroalaEditor(div.dom, EditArticle.getFroalaOptions(), function() {
|
||||
vnode.state.froala.html.set(vnode.state.article.description)
|
||||
})
|
||||
oncreate: (div) => {
|
||||
console.log(div.dom)
|
||||
},
|
||||
})
|
||||
: null
|
||||
|
@ -324,37 +299,49 @@ const EditArticle = {
|
|||
m('label', 'Published at'),
|
||||
m('input', {
|
||||
type: 'datetime-local',
|
||||
value: this.article.published_at,
|
||||
oninput: this.updateValue.bind(this, 'published_at'),
|
||||
value: this.data.article.publish_at,
|
||||
oninput: this.updateValue.bind(this, 'publish_at'),
|
||||
}),
|
||||
m('label', 'Published by'),
|
||||
m('select', {
|
||||
onchange: this.updateStaffer.bind(this),
|
||||
}, staffers.map(function(item) { return m('option', { value: item.id, selected: item.id === vnode.state.article.staff_id }, item.name) })),
|
||||
},
|
||||
this.data.staff.map((item) => {
|
||||
return m('option', {
|
||||
value: item.id,
|
||||
selected: item.id === this.data.article.staff_id
|
||||
}, item.name)
|
||||
})
|
||||
),
|
||||
m('label', 'Make featured'),
|
||||
m('input', {
|
||||
type: 'checkbox',
|
||||
checked: this.article.is_featured,
|
||||
checked: this.data.article.is_featured,
|
||||
oninput: this.updateValue.bind(this, 'is_featured'),
|
||||
}),
|
||||
m('div.loading-spinner', { hidden: this.loadedFroala }),
|
||||
m('div', [
|
||||
m('input', {
|
||||
type: 'submit',
|
||||
value: 'Save',
|
||||
}),
|
||||
showPublish
|
||||
? m('button.submit', { onclick: function() { vnode.state.article.published_at = new Date().toISOString() }}, 'Publish')
|
||||
? m('button.submit', {
|
||||
onclick: () => {
|
||||
this.data.article.publish_at = new Date().toISOString()
|
||||
}
|
||||
}, 'Publish')
|
||||
: null,
|
||||
]),
|
||||
]),
|
||||
this.article.files.length
|
||||
this.data.files.length
|
||||
? m('files', [
|
||||
m('h4', 'Files'),
|
||||
this.article.files.map(function(item) { return m(Fileinfo, { file: item }) }),
|
||||
this.data.files.map((file) => {
|
||||
return m(Fileinfo, { file: file })
|
||||
}),
|
||||
])
|
||||
: null,
|
||||
this.article.id
|
||||
this.data.article.id
|
||||
? m('div.fileupload', [
|
||||
'Add file',
|
||||
m('input', {
|
||||
|
@ -367,7 +354,8 @@ const EditArticle = {
|
|||
: null,
|
||||
]),
|
||||
])
|
||||
)
|
||||
: null,
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -17,10 +17,10 @@ const EditStaff = {
|
|||
this.creating = this.lastid === 'add'
|
||||
this.error = ''
|
||||
this.staff = {
|
||||
fullname: '',
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
level: 10,
|
||||
rank: 10,
|
||||
}
|
||||
|
||||
if (this.lastid !== 'add') {
|
||||
|
@ -28,7 +28,7 @@ const EditStaff = {
|
|||
.then(function(result) {
|
||||
vnode.state.editedPath = true
|
||||
vnode.state.staff = result
|
||||
document.title = 'Editing: ' + result.fullname + ' - Admin NFP Moe'
|
||||
document.title = 'Editing: ' + result.name + ' - Admin NFP Moe'
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
|
@ -42,13 +42,13 @@ const EditStaff = {
|
|||
}
|
||||
},
|
||||
|
||||
updateValue: function(fullname, e) {
|
||||
this.staff[fullname] = e.currentTarget.value
|
||||
updateValue: function(key, e) {
|
||||
this.staff[key] = e.currentTarget.value
|
||||
},
|
||||
|
||||
save: function(vnode, e) {
|
||||
e.preventDefault()
|
||||
if (!this.staff.fullname) {
|
||||
if (!this.staff.name) {
|
||||
this.error = 'Fullname is missing'
|
||||
} else if (!this.staff.email) {
|
||||
this.error = 'Email is missing'
|
||||
|
@ -65,16 +65,16 @@ const EditStaff = {
|
|||
|
||||
if (this.staff.id) {
|
||||
promise = Staff.updateStaff(this.staff.id, {
|
||||
fullname: this.staff.fullname,
|
||||
name: this.staff.name,
|
||||
email: this.staff.email,
|
||||
level: this.staff.level,
|
||||
rank: this.staff.rank,
|
||||
password: this.staff.password,
|
||||
})
|
||||
} else {
|
||||
promise = Staff.createStaff({
|
||||
fullname: this.staff.fullname,
|
||||
name: this.staff.name,
|
||||
email: this.staff.email,
|
||||
level: this.staff.level,
|
||||
rank: this.staff.rank,
|
||||
password: this.staff.password,
|
||||
})
|
||||
}
|
||||
|
@ -92,11 +92,11 @@ const EditStaff = {
|
|||
},
|
||||
|
||||
updateLevel: function(e) {
|
||||
this.staff.level = Number(e.currentTarget.value)
|
||||
this.staff.rank = Number(e.currentTarget.value)
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
const levels = [[10, 'Manager'], [100, 'Admin']]
|
||||
const ranks = [[10, 'Manager'], [100, 'Admin']]
|
||||
return (
|
||||
this.loading ?
|
||||
m('div.loading-spinner')
|
||||
|
@ -108,7 +108,7 @@ const EditStaff = {
|
|||
]
|
||||
: null),
|
||||
m('article.editstaff', [
|
||||
m('header', m('h1', this.creating ? 'Create Staff' : 'Edit ' + (this.staff.fullname || '(untitled)'))),
|
||||
m('header', m('h1', this.creating ? 'Create Staff' : 'Edit ' + (this.staff.name || '(untitled)'))),
|
||||
m('div.error', {
|
||||
hidden: !this.error,
|
||||
onclick: function() { vnode.state.error = '' },
|
||||
|
@ -119,12 +119,12 @@ const EditStaff = {
|
|||
m('label', 'Level'),
|
||||
m('select', {
|
||||
onchange: this.updateLevel.bind(this),
|
||||
}, levels.map(function(level) { return m('option', { value: level[0], selected: level[0] === vnode.state.staff.level }, level[1]) })),
|
||||
}, ranks.map(function(rank) { return m('option', { value: rank[0], selected: rank[0] === vnode.state.staff.rank }, rank[1]) })),
|
||||
m('label', 'Fullname'),
|
||||
m('input', {
|
||||
type: 'text',
|
||||
value: this.staff.fullname,
|
||||
oninput: this.updateValue.bind(this, 'fullname'),
|
||||
value: this.staff.name,
|
||||
oninput: this.updateValue.bind(this, 'name'),
|
||||
}),
|
||||
m('label', 'Email'),
|
||||
m('input', {
|
||||
|
|
|
@ -77,9 +77,9 @@ const AdminStaffList = {
|
|||
),
|
||||
m('tbody', this.staff.map(function(item) {
|
||||
return m('tr', [
|
||||
m('td', m(m.route.Link, { href: '/admin/staff/' + item.id }, item.fullname)),
|
||||
m('td', m(m.route.Link, { href: '/admin/staff/' + item.id }, item.name)),
|
||||
m('td', item.email),
|
||||
m('td.right', AdminStaffList.getLevel(item.level)),
|
||||
m('td.right', AdminStaffList.getLevel(item.rank)),
|
||||
m('td.right', (item.updated_at || '---').replace('T', ' ').split('.')[0]),
|
||||
m('td.right', m('button', { onclick: function() { vnode.state.removeStaff = item } }, 'Remove')),
|
||||
])
|
||||
|
@ -95,7 +95,7 @@ const AdminStaffList = {
|
|||
m(Dialogue, {
|
||||
hidden: vnode.state.removeStaff === null,
|
||||
title: 'Delete ' + (vnode.state.removeStaff ? vnode.state.removeStaff.name : ''),
|
||||
message: 'Are you sure you want to remove "' + (vnode.state.removeStaff ? vnode.state.removeStaff.fullname : '') + '" (' + (vnode.state.removeStaff ? vnode.state.removeStaff.email : '') + ')',
|
||||
message: 'Are you sure you want to remove "' + (vnode.state.removeStaff ? vnode.state.removeStaff.name : '') + '" (' + (vnode.state.removeStaff ? vnode.state.removeStaff.email : '') + ')',
|
||||
yes: 'Remove',
|
||||
yesclass: 'alert',
|
||||
no: 'Cancel',
|
||||
|
|
|
@ -1,46 +1,8 @@
|
|||
const common = require('./common')
|
||||
|
||||
exports.getAllArticlesPagination = function(options) {
|
||||
let extra = ''
|
||||
|
||||
if (options.sort) {
|
||||
extra += '&sort=' + options.sort
|
||||
}
|
||||
if (options.per_page) {
|
||||
extra += '&perPage=' + options.per_page
|
||||
}
|
||||
if (options.page) {
|
||||
extra += '&page=' + options.page
|
||||
}
|
||||
if (options.includes) {
|
||||
extra += '&includes=' + options.includes.join(',')
|
||||
}
|
||||
|
||||
return '/api/articles/public?' + extra
|
||||
}
|
||||
|
||||
exports.getAllPageArticlesPagination = function(pageId, options) {
|
||||
let extra = ''
|
||||
|
||||
if (options.sort) {
|
||||
extra += '&sort=' + options.sort
|
||||
}
|
||||
if (options.per_page) {
|
||||
extra += '&perPage=' + options.per_page
|
||||
}
|
||||
if (options.page) {
|
||||
extra += '&page=' + options.page
|
||||
}
|
||||
if (options.includes) {
|
||||
extra += '&includes=' + options.includes.join(',')
|
||||
}
|
||||
|
||||
return '/api/pages/' + pageId + '/articles/public?' + extra
|
||||
}
|
||||
|
||||
exports.getArticle = function(id) {
|
||||
return common.sendRequest({
|
||||
method: 'GET',
|
||||
url: '/api/articles/public/' + id + '?includes=media,parent,banner,files,staff',
|
||||
url: '/api/articles/' + id,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ exports.sendRequest = function(options, isPagination) {
|
|||
|
||||
return m.request(options)
|
||||
.catch(function (error) {
|
||||
if (error.code === 403) {
|
||||
if (error.status === 403) {
|
||||
Authentication.clearToken()
|
||||
m.route.set('/login', { redirect: m.route.get() })
|
||||
}
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
const common = require('./common')
|
||||
|
||||
const Tree = window.__nfptree && window.__nfptree.tree || []
|
||||
const TreeMap = new Map()
|
||||
|
||||
exports.Tree = Tree
|
||||
exports.TreeMap = TreeMap
|
||||
|
||||
exports.getTree = function() {
|
||||
return common.sendRequest({
|
||||
method: 'GET',
|
||||
url: '/api/pagetree',
|
||||
})
|
||||
function parseLeaf(tree) {
|
||||
for (let branch of tree) {
|
||||
TreeMap.set(branch.path, branch)
|
||||
|
||||
if (branch.children && branch.children.length) {
|
||||
parseLeaf(branch.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseLeaf(Tree)
|
||||
|
||||
exports.getPage = function(path, page) {
|
||||
return common.sendRequest({
|
||||
method: 'GET',
|
||||
|
|
|
@ -73,6 +73,7 @@ main {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error {
|
||||
|
|
|
@ -6,68 +6,77 @@ const Fileinfo = require('../widgets/fileinfo')
|
|||
const Article = {
|
||||
oninit: function(vnode) {
|
||||
this.error = ''
|
||||
this.lastarticle = m.route.param('article') || '1'
|
||||
this.loading = false
|
||||
this.showLoading = null
|
||||
this.data = {
|
||||
article: null,
|
||||
files: [],
|
||||
}
|
||||
this.showcomments = false
|
||||
|
||||
if (window.__nfpdata) {
|
||||
this.path = m.route.param('id')
|
||||
this.article = window.__nfpdata
|
||||
this.data.article = window.__nfpdata
|
||||
window.__nfpdata = null
|
||||
} else {
|
||||
this.fetchArticle(vnode)
|
||||
}
|
||||
},
|
||||
|
||||
fetchArticle: function(vnode) {
|
||||
this.error = ''
|
||||
this.path = m.route.param('id')
|
||||
this.showcomments = false
|
||||
this.article = {
|
||||
id: 0,
|
||||
name: '',
|
||||
path: '',
|
||||
description: '',
|
||||
media: null,
|
||||
banner: null,
|
||||
files: [],
|
||||
}
|
||||
this.loading = true
|
||||
|
||||
ApiArticle.getArticle(this.path)
|
||||
.then(function(result) {
|
||||
vnode.state.article = result
|
||||
if (result.parent) {
|
||||
document.title = result.name + ' - ' + result.parent.name + ' - NFP Moe'
|
||||
} else {
|
||||
document.title = result.name + ' - NFP Moe'
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
vnode.state.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
onbeforeupdate: function(vnode) {
|
||||
if (this.path !== m.route.param('id')) {
|
||||
this.fetchArticle(vnode)
|
||||
}
|
||||
},
|
||||
|
||||
fetchArticle: function(vnode) {
|
||||
this.error = ''
|
||||
this.path = m.route.param('id')
|
||||
this.showcomments = false
|
||||
|
||||
if (this.showLoading) {
|
||||
clearTimeout(this.showLoading)
|
||||
}
|
||||
|
||||
if (this.data.article) {
|
||||
this.showLoading = setTimeout(() => {
|
||||
this.showLoading = null
|
||||
this.loading = true
|
||||
m.redraw()
|
||||
}, 150)
|
||||
} else {
|
||||
this.loading = true
|
||||
}
|
||||
|
||||
ApiArticle.getArticle(this.path)
|
||||
.then((result) => {
|
||||
this.data = result
|
||||
|
||||
if (!this.data.article) {
|
||||
this.error = 'Article not found'
|
||||
}
|
||||
}, (err) => {
|
||||
this.error = err.message
|
||||
})
|
||||
.then(() => {
|
||||
clearTimeout(this.showLoading)
|
||||
this.showLoading = null
|
||||
this.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
var deviceWidth = window.innerWidth
|
||||
var imagePath = ''
|
||||
|
||||
if (this.article.media) {
|
||||
if (this.data.article && this.data.article.media) {
|
||||
var pixelRatio = window.devicePixelRatio || 1
|
||||
if ((deviceWidth < 800 && pixelRatio <= 1)
|
||||
|| (deviceWidth < 600 && pixelRatio > 1)) {
|
||||
imagePath = this.article.media.medium_url
|
||||
imagePath = this.data.article.media.medium_url
|
||||
} else {
|
||||
imagePath = this.article.media.large_url
|
||||
imagePath = this.data.article.media.large_url
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,33 +91,37 @@ const Article = {
|
|||
},
|
||||
}, 'Article error: ' + this.error))
|
||||
: m('article.article', [
|
||||
this.article.parent ? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + this.article.parent.path }, this.article.parent.name)]) : null,
|
||||
m('header', m('h1', this.article.name)),
|
||||
this.data.article.page_path
|
||||
? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + this.data.article.page_path }, this.data.article.page_name)])
|
||||
: null,
|
||||
m('header', m('h1', this.data.article.name)),
|
||||
m('.fr-view', [
|
||||
this.article.media
|
||||
this.data.article.media
|
||||
? m('a.cover', {
|
||||
rel: 'noopener',
|
||||
href: this.article.media.link,
|
||||
}, m('img', { src: imagePath, alt: 'Cover image for ' + this.article.name }))
|
||||
href: this.data.article.media.link,
|
||||
}, m('img', { src: imagePath, alt: 'Cover image for ' + this.data.article.name }))
|
||||
: null,
|
||||
this.article.description ? m.trust(this.article.description) : null,
|
||||
(this.article.files && this.article.files.length
|
||||
? this.article.files.map(function(file) {
|
||||
this.data.article.content ? m.trust(this.data.article.content) : null,
|
||||
this.data.files.map(function(file) {
|
||||
return m(Fileinfo, { file: file })
|
||||
})
|
||||
: null),
|
||||
}),
|
||||
m('div.entrymeta', [
|
||||
'Posted ',
|
||||
(this.article.parent ? 'in' : ''),
|
||||
(this.article.parent ? m(m.route.Link, { href: '/page/' + this.article.parent.path }, this.article.parent.name) : null),
|
||||
'at ' + (this.article.published_at.replace('T', ' ').split('.')[0]).substr(0, 16),
|
||||
' by ' + (this.article.staff && this.article.staff.fullname || 'Admin'),
|
||||
this.data.article.page_path
|
||||
? [
|
||||
'in',
|
||||
m(m.route.Link, { href: '/page/' + this.data.article.page_path }, this.data.article.page_name)
|
||||
]
|
||||
: '',
|
||||
'at ' + (this.data.article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16),
|
||||
' by ' + (this.data.article.admin_name || 'Admin'),
|
||||
]),
|
||||
]),
|
||||
Authentication.currentUser
|
||||
? m('div.admin-actions', [
|
||||
m('span', 'Admin controls:'),
|
||||
m(m.route.Link, { href: '/admin/articles/' + this.article.id }, 'Edit article'),
|
||||
m(m.route.Link, { href: '/admin/articles/' + this.data.article.path }, 'Edit article'),
|
||||
])
|
||||
: null,
|
||||
this.showcomments
|
||||
|
|
|
@ -10,92 +10,87 @@ const Frontpage = {
|
|||
oninit: function(vnode) {
|
||||
this.error = ''
|
||||
this.loading = false
|
||||
this.featured = null
|
||||
this.links = null
|
||||
|
||||
if (window.__nfpfeatured) {
|
||||
this.featured = window.__nfpfeatured
|
||||
this.showLoading = null
|
||||
this.data = {
|
||||
page: null,
|
||||
articles: [],
|
||||
total_articles: 0,
|
||||
featured: null,
|
||||
}
|
||||
this.currentPage = Number(m.route.param('page')) || 1
|
||||
|
||||
if (window.__nfpdata
|
||||
&& window.__nfplinks) {
|
||||
this.links = window.__nfplinks
|
||||
this.articles = window.__nfpdata
|
||||
this.lastpage = m.route.param('page') || '1'
|
||||
if (window.__nfpdata) {
|
||||
this.lastpage = this.currentPage
|
||||
window.__nfpdata = null
|
||||
window.__nfplinks = null
|
||||
|
||||
if (this.articles.length === 0) {
|
||||
m.route.set('/')
|
||||
} else {
|
||||
Frontpage.processFeatured(vnode, this.articles)
|
||||
}
|
||||
} else {
|
||||
this.fetchArticles(vnode)
|
||||
this.fetchPage(vnode)
|
||||
}
|
||||
},
|
||||
|
||||
onupdate: function(vnode) {
|
||||
if (this.lastpage !== (m.route.param('page') || '1')) {
|
||||
this.fetchArticles(vnode)
|
||||
m.redraw()
|
||||
onbeforeupdate: function(vnode) {
|
||||
this.currentPage = Number(m.route.param('page')) || 1
|
||||
if (this.lastpage !== this.currentPage) {
|
||||
this.fetchPage(vnode)
|
||||
}
|
||||
},
|
||||
|
||||
fetchArticles: function(vnode) {
|
||||
fetchPage: function(vnode) {
|
||||
this.error = ''
|
||||
this.loading = true
|
||||
this.links = null
|
||||
this.articles = []
|
||||
this.lastpage = m.route.param('page') || '1'
|
||||
this.lastpage = this.currentPage
|
||||
|
||||
if (this.lastpage !== '1') {
|
||||
if (this.showLoading) {
|
||||
clearTimeout(this.showLoading)
|
||||
}
|
||||
|
||||
this.showLoading = setTimeout(() => {
|
||||
this.showLoading = null
|
||||
this.loading = true
|
||||
m.redraw()
|
||||
}, 150)
|
||||
|
||||
if (this.lastpage !== 1) {
|
||||
document.title = 'Page ' + this.lastpage + ' - NFP Moe - Anime/Manga translation group'
|
||||
} else {
|
||||
document.title = 'NFP Moe - Anime/Manga translation group'
|
||||
}
|
||||
|
||||
return Page.getPage(null, this.lastpage)
|
||||
.then(function(result) {
|
||||
console.log(result)
|
||||
.then((result) => {
|
||||
this.data = result
|
||||
}, (err) => {
|
||||
this.error = err.message
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
vnode.state.loading = false
|
||||
.then(() => {
|
||||
clearTimeout(this.showLoading)
|
||||
this.showLoading = null
|
||||
this.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
processFeatured: function(vnode, data) {
|
||||
if (vnode.state.featured) return
|
||||
for (var i = data.length - 1; i >= 0; i--) {
|
||||
if (data[i].banner) {
|
||||
vnode.state.featured = data[i]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
var deviceWidth = window.innerWidth
|
||||
var bannerPath = ''
|
||||
|
||||
if (this.featured && this.featured.banner) {
|
||||
if (this.data.featured && this.data.featured.banner) {
|
||||
var pixelRatio = window.devicePixelRatio || 1
|
||||
if (deviceWidth < 400 && pixelRatio <= 1) {
|
||||
bannerPath = window.supportsavif
|
||||
&& this.featured.banner.small_url_avif
|
||||
|| this.featured.banner.small_url
|
||||
&& this.data.featured.banner.small_url_avif
|
||||
|| this.data.featured.banner.small_url
|
||||
} else if ((deviceWidth < 800 && pixelRatio <= 1)
|
||||
|| (deviceWidth < 600 && pixelRatio > 1)) {
|
||||
bannerPath = window.supportsavif
|
||||
&& this.featured.banner.medium_url_avif
|
||||
|| this.featured.banner.medium_url
|
||||
&& this.data.featured.banner.medium_url_avif
|
||||
|| this.data.featured.banner.medium_url
|
||||
} else {
|
||||
bannerPath = window.supportsavif
|
||||
&& this.featured.banner.large_url_avif
|
||||
|| this.featured.banner.large_url
|
||||
&& this.data.featured.banner.large_url_avif
|
||||
|| this.data.featured.banner.large_url
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,10 +98,10 @@ const Frontpage = {
|
|||
(bannerPath
|
||||
? m(m.route.Link, {
|
||||
class: 'frontpage-banner',
|
||||
href: '/article/' + this.featured.path,
|
||||
href: '/article/' + this.data.featured.path,
|
||||
style: { 'background-image': 'url("' + bannerPath + '")' },
|
||||
},
|
||||
this.featured.name
|
||||
this.data.featured.name
|
||||
)
|
||||
: null),
|
||||
m('frontpage', [
|
||||
|
@ -134,12 +129,13 @@ const Frontpage = {
|
|||
(this.loading
|
||||
? m('div.loading-spinner')
|
||||
: null),
|
||||
this.articles.map(function(article) {
|
||||
return m(Newsitem, article)
|
||||
this.data.articles.map(function(article) {
|
||||
return m(Newsitem, { article: article })
|
||||
}),
|
||||
m(Pages, {
|
||||
base: '/',
|
||||
links: this.links,
|
||||
total: this.data.total_articles,
|
||||
page: this.currentPage,
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
|
|
17
app/index.js
17
app/index.js
|
@ -34,12 +34,13 @@ const onLoaded = function() {
|
|||
loaded++
|
||||
if (loaded < 2) return
|
||||
|
||||
Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.level >= 10)
|
||||
Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.rank >= 10)
|
||||
loadedAdmin = true
|
||||
m.route.set(m.route.get())
|
||||
}
|
||||
|
||||
const onError = function() {
|
||||
const onError = function(a, b, c) {
|
||||
console.log('onError', this, a, b, c)
|
||||
elements.forEach(function(x) { x.remove() })
|
||||
loadedAdmin = loadingAdmin = false
|
||||
loaded = 0
|
||||
|
@ -49,11 +50,11 @@ const onError = function() {
|
|||
const loadAdmin = function(user) {
|
||||
if (loadingAdmin) {
|
||||
if (loadedAdmin) {
|
||||
Authentication.setAdmin(user && user.level >= 10)
|
||||
Authentication.setAdmin(user && user.rank >= 10)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!user || user.level < 10) return
|
||||
if (!user || user.rank < 10) return
|
||||
|
||||
loadingAdmin = true
|
||||
|
||||
|
@ -74,6 +75,14 @@ const loadAdmin = function(user) {
|
|||
element.onload = onLoaded
|
||||
element.onerror = onError
|
||||
document.body.appendChild(element)
|
||||
|
||||
element = document.createElement('script')
|
||||
elements.push(element)
|
||||
element.setAttribute('type', 'text/javascript')
|
||||
element.setAttribute('src', '/assets/editor.js')
|
||||
element.onload = onLoaded
|
||||
element.onerror = onError
|
||||
document.body.appendChild(element)
|
||||
}
|
||||
|
||||
Authentication.addEvent(loadAdmin)
|
||||
|
|
|
@ -35,13 +35,17 @@ const Login = {
|
|||
|
||||
Api.sendRequest({
|
||||
method: 'POST',
|
||||
url: '/api/login/user',
|
||||
url: '/api/authentication/login',
|
||||
body: {
|
||||
username: this.username,
|
||||
email: this.username,
|
||||
password: this.password,
|
||||
},
|
||||
})
|
||||
.then(function(result) {
|
||||
if (!result.token) {
|
||||
return Promise.reject(new Error('Server authentication down.'))
|
||||
}
|
||||
console.log(result)
|
||||
Authentication.updateToken(result.token)
|
||||
m.route.set(Login.redirect || '/')
|
||||
})
|
||||
|
|
|
@ -37,7 +37,6 @@ const Menu = {
|
|||
},
|
||||
|
||||
view: function() {
|
||||
console.log(Page.Tree)
|
||||
return [
|
||||
m('div.top', [
|
||||
m(m.route.Link,
|
||||
|
@ -46,7 +45,7 @@ const Menu = {
|
|||
),
|
||||
m('aside', Authentication.currentUser ? [
|
||||
m('p', [
|
||||
'Welcome ' + Authentication.currentUser.email,
|
||||
'Welcome ' + Authentication.currentUser.name,
|
||||
m(m.route.Link, { href: '/logout' }, 'Logout'),
|
||||
(Darkmode.darkIsOn
|
||||
? m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, false) }, 'Day mode')
|
||||
|
@ -58,9 +57,9 @@ const Menu = {
|
|||
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'),
|
||||
m(m.route.Link, { hidden: Authentication.currentUser.level < 100, href: '/admin/staff' }, 'Staff'),
|
||||
m(m.route.Link, { hidden: Authentication.currentUser.rank < 100, href: '/admin/staff' }, 'Staff'),
|
||||
])
|
||||
: (Authentication.currentUser.level > 10 ? m('div.loading-spinner') : null)
|
||||
: (Authentication.currentUser.rank > 10 ? m('div.loading-spinner') : null)
|
||||
),
|
||||
] : (Darkmode.darkIsOn
|
||||
? m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, false) }, 'Day mode')
|
||||
|
|
|
@ -9,15 +9,20 @@ const Pages = require('../widgets/pages')
|
|||
const Page = {
|
||||
oninit: function(vnode) {
|
||||
this.error = ''
|
||||
this.lastpage = m.route.param('page') || '1'
|
||||
this.loadingnews = false
|
||||
this.loading = false
|
||||
this.showLoading = null
|
||||
this.data = {
|
||||
page: null,
|
||||
articles: [],
|
||||
total_articles: 0,
|
||||
featured: null,
|
||||
}
|
||||
this.children = []
|
||||
this.currentPage = Number(m.route.param('page')) || 1
|
||||
|
||||
console.log(window.__nfpdata)
|
||||
if (window.__nfpdata) {
|
||||
this.path = m.route.param('id')
|
||||
this.page = window.__nfpdata
|
||||
this.news = window.__nfpsubdata
|
||||
this.newslinks = window.__nfplinks
|
||||
this.data = window.__nfpdata
|
||||
|
||||
window.__nfpdata = null
|
||||
window.__nfpsubdata = null
|
||||
|
@ -26,60 +31,57 @@ const Page = {
|
|||
}
|
||||
},
|
||||
|
||||
fetchPage: function(vnode) {
|
||||
this.path = m.route.param('id')
|
||||
this.news = []
|
||||
this.newslinks = null
|
||||
this.page = {
|
||||
id: 0,
|
||||
name: '',
|
||||
path: '',
|
||||
description: '',
|
||||
media: null,
|
||||
}
|
||||
this.loading = true
|
||||
this.loadingnews = true
|
||||
|
||||
ApiPage.getPage(this.path)
|
||||
.then(function(result) {
|
||||
vnode.state.page = result
|
||||
document.title = result.name + ' - NFP Moe'
|
||||
return vnode.state.fetchArticles(vnode)
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
vnode.state.loading = vnode.state.loadingnews = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
onbeforeupdate: function(vnode) {
|
||||
if (this.path !== m.route.param('id')) {
|
||||
this.currentPage = Number(m.route.param('page')) || 1
|
||||
|
||||
if (this.path !== m.route.param('id') || this.currentPage !== this.lastpage) {
|
||||
this.fetchPage(vnode)
|
||||
} else if (m.route.param('page') && m.route.param('page') !== this.lastpage) {
|
||||
this.fetchArticles(vnode)
|
||||
}
|
||||
},
|
||||
|
||||
fetchArticles: function(vnode) {
|
||||
this.loadingnews = true
|
||||
this.newslinks = null
|
||||
this.lastpage = m.route.param('page') || '1'
|
||||
fetchPage: function(vnode) {
|
||||
this.error = ''
|
||||
this.lastpage = this.currentPage
|
||||
this.path = m.route.param('id')
|
||||
|
||||
return pagination.fetchPage(Article.getAllPageArticlesPagination(this.page.id, {
|
||||
per_page: 10,
|
||||
page: this.lastpage,
|
||||
includes: ['files', 'media'],
|
||||
}))
|
||||
.then(function(result) {
|
||||
vnode.state.news = result.data
|
||||
vnode.state.newslinks = result.links
|
||||
if (this.showLoading) {
|
||||
clearTimeout(this.showLoading)
|
||||
}
|
||||
|
||||
if (this.data.page) {
|
||||
this.showLoading = setTimeout(() => {
|
||||
this.showLoading = null
|
||||
this.loading = true
|
||||
m.redraw()
|
||||
}, 150)
|
||||
} else {
|
||||
this.loading = true
|
||||
}
|
||||
|
||||
this.children = ApiPage.TreeMap.get(this.path)
|
||||
this.children = this.children && this.children.children || []
|
||||
|
||||
ApiPage.getPage(this.path, this.lastpage)
|
||||
.then((result) => {
|
||||
this.data = result
|
||||
|
||||
if (!this.data.page) {
|
||||
this.error = 'Page not found'
|
||||
return
|
||||
}
|
||||
|
||||
if (this.lastpage !== 1) {
|
||||
document.title = 'Page ' + this.lastpage + ' - ' + this.data.page.name + ' - NFP Moe'
|
||||
} else {
|
||||
document.title = this.data.page.name + ' - NFP Moe'
|
||||
}
|
||||
}, (err) => {
|
||||
this.error = err.message
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
vnode.state.loading = vnode.state.loadingnews = false
|
||||
.then(() => {
|
||||
clearTimeout(this.showLoading)
|
||||
this.showLoading = null
|
||||
this.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
@ -90,94 +92,106 @@ const Page = {
|
|||
var bannerPath = ''
|
||||
var imagePath = ''
|
||||
|
||||
if (this.page && this.page.banner) {
|
||||
if (this.data.page && this.data.page.banner) {
|
||||
if (deviceWidth < 400 && pixelRatio <= 1) {
|
||||
bannerPath = this.page.banner.small_url
|
||||
bannerPath = this.data.page.banner.small_url
|
||||
} else if ((deviceWidth < 800 && pixelRatio <= 1)
|
||||
|| (deviceWidth < 600 && pixelRatio > 1)) {
|
||||
bannerPath = this.page.banner.medium_url
|
||||
bannerPath = this.data.page.banner.medium_url
|
||||
} else {
|
||||
bannerPath = this.page.banner.large_url
|
||||
bannerPath = this.data.page.banner.large_url
|
||||
}
|
||||
}
|
||||
|
||||
if (this.page && this.page.media) {
|
||||
if (this.data.page && this.data.page.media) {
|
||||
if ((deviceWidth < 1000 && pixelRatio <= 1)
|
||||
|| (deviceWidth < 800 && pixelRatio > 1)) {
|
||||
imagePath = this.page.media.medium_url
|
||||
imagePath = this.data.page.media.medium_url
|
||||
} else {
|
||||
imagePath = this.page.media.large_url
|
||||
imagePath = this.data.page.media.large_url
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
this.loading ?
|
||||
m('article.page', m('div.loading-spinner'))
|
||||
: this.error
|
||||
return ([
|
||||
this.loading
|
||||
? m('article.page', m('div.loading-spinner'))
|
||||
: null,
|
||||
!this.loading && this.error
|
||||
? m('div.error-wrapper', m('div.error', {
|
||||
onclick: function() {
|
||||
vnode.state.error = ''
|
||||
vnode.state.fetchPage(vnode)
|
||||
},
|
||||
}, 'Article error: ' + this.error))
|
||||
: m('article.page', [
|
||||
bannerPath ? m('.div.page-banner', { style: { 'background-image': 'url("' + bannerPath + '")' } } ) : null,
|
||||
this.page.parent
|
||||
? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + this.page.parent.path }, this.page.parent.name)])
|
||||
: m('div.goback', ['« ', m(m.route.Link, { href: '/' }, 'Home')]),
|
||||
m('header', m('h1', this.page.name)),
|
||||
}, 'Page error: ' + this.error))
|
||||
: null,
|
||||
!this.loading && !this.error
|
||||
? m('article.page', [
|
||||
bannerPath
|
||||
? m('.div.page-banner', { style: { 'background-image': 'url("' + bannerPath + '")' } } )
|
||||
: null,
|
||||
m('div.goback', ['« ', m(m.route.Link, {
|
||||
href: this.data.page.parent_path
|
||||
? '/page/' + this.data.page.parent_path
|
||||
: '/'
|
||||
}, this.data.page.parent_name || 'Home')]),
|
||||
m('header', m('h1', this.data.page.name)),
|
||||
m('.container', {
|
||||
class: this.page.children.length ? 'multi' : '',
|
||||
class: this.children.length ? 'multi' : '',
|
||||
}, [
|
||||
this.page.children.length
|
||||
this.children.length
|
||||
? m('aside.sidebar', [
|
||||
m('h4', 'View ' + this.page.name + ':'),
|
||||
this.page.children.map(function(page) {
|
||||
m('h4', 'View ' + this.data.page.name + ':'),
|
||||
this.children.map(function(page) {
|
||||
return m(m.route.Link, { href: '/page/' + page.path }, page.name)
|
||||
}),
|
||||
])
|
||||
: null,
|
||||
this.page.description
|
||||
this.data.page.content
|
||||
? m('.fr-view', [
|
||||
imagePath ? m('a', { href: this.page.media.link}, m('img.page-cover', { src: imagePath, alt: 'Cover image for ' + this.page.name } )) : null,
|
||||
m.trust(this.page.description),
|
||||
this.news.length && this.page.description
|
||||
imagePath
|
||||
? m('a', { href: this.data.page.media.link}, m('img.page-cover', { src: imagePath, alt: 'Cover image for ' + this.data.page.name } ))
|
||||
: null,
|
||||
m.trust(this.data.page.content),
|
||||
this.data.articles.length && this.data.page.content
|
||||
? m('aside.news', [
|
||||
m('h4', 'Latest posts under ' + this.page.name + ':'),
|
||||
this.loadingnews ? m('div.loading-spinner') : this.news.map(function(article) {
|
||||
m('h4', 'Latest posts under ' + this.data.page.name + ':'),
|
||||
this.data.articles.map(function(article) {
|
||||
return m(Newsentry, article)
|
||||
}),
|
||||
m(Pages, {
|
||||
base: '/page/' + this.page.path,
|
||||
links: this.newslinks,
|
||||
base: '/page/' + this.data.page.path,
|
||||
total: this.data.total_articles,
|
||||
page: this.currentPage,
|
||||
}),
|
||||
])
|
||||
: null,
|
||||
])
|
||||
: this.news.length
|
||||
: this.data.articles.length
|
||||
? m('aside.news.single', [
|
||||
imagePath ? m('a', { href: this.page.media.link}, m('img.page-cover', { src: imagePath, alt: 'Cover image for ' + this.page.name } )) : null,
|
||||
m('h4', 'Latest posts under ' + this.page.name + ':'),
|
||||
this.loadingnews ? m('div.loading-spinner') : this.news.map(function(article) {
|
||||
imagePath ? m('a', { href: this.data.page.media.link}, m('img.page-cover', { src: imagePath, alt: 'Cover image for ' + this.data.page.name } )) : null,
|
||||
m('h4', 'Latest posts under ' + this.data.page.name + ':'),
|
||||
this.data.articles.map(function(article) {
|
||||
return m(Newsentry, article)
|
||||
}),
|
||||
m(Pages, {
|
||||
base: '/page/' + this.page.path,
|
||||
links: this.newslinks,
|
||||
base: '/page/' + this.data.page.path,
|
||||
total: this.data.total_articles,
|
||||
page: this.currentPage,
|
||||
}),
|
||||
])
|
||||
: this.page.media
|
||||
? m('img.page-cover.single', { src: this.page.media.medium_url, alt: 'Cover image for ' + this.page.name } )
|
||||
: this.data.page.media
|
||||
? m('img.page-cover.single', { src: this.data.page.media.medium_url, alt: 'Cover image for ' + this.data.page.name } )
|
||||
: null,
|
||||
]),
|
||||
Authentication.currentUser
|
||||
? m('div.admin-actions', [
|
||||
m('span', 'Admin controls:'),
|
||||
m(m.route.Link, { href: '/admin/pages/' + this.page.id }, 'Edit page'),
|
||||
m(m.route.Link, { href: '/admin/pages/' + this.data.page.path }, 'Edit page'),
|
||||
])
|
||||
: null,
|
||||
])
|
||||
: null,
|
||||
])
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,38 +1,32 @@
|
|||
const Media = require('../api/media')
|
||||
|
||||
const FileUpload = {
|
||||
uploadFile: function(vnode, event) {
|
||||
fileChanged: function(vnode, event) {
|
||||
if (!event.target.files[0]) return
|
||||
vnode.state.updateError(vnode, '')
|
||||
vnode.state.loading = true
|
||||
|
||||
Media.uploadMedia(event.target.files[0], vnode.attrs.height || null)
|
||||
.then(function(res) {
|
||||
if (vnode.attrs.onupload) {
|
||||
vnode.attrs.onupload(res)
|
||||
let preview = null
|
||||
if (event.target.files[0].type.startsWith('image')) {
|
||||
preview = URL.createObjectURL(event.target.files[0])
|
||||
}
|
||||
if (this.preview) {
|
||||
this.preview.clear()
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.updateError(vnode, err.message)
|
||||
})
|
||||
.then(function() {
|
||||
event.target.value = null
|
||||
vnode.state.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
updateError: function(vnode, error) {
|
||||
if (vnode.attrs.onerror) {
|
||||
vnode.attrs.onerror(error)
|
||||
} else {
|
||||
vnode.state.error = error
|
||||
let out = {
|
||||
file: event.target.files[0],
|
||||
preview: preview,
|
||||
clear: function() {
|
||||
URL.revokeObjectURL(preview)
|
||||
}
|
||||
}
|
||||
|
||||
this.preview = out
|
||||
|
||||
vnode.attrs.onfile(out)
|
||||
},
|
||||
|
||||
oninit: function(vnode) {
|
||||
vnode.state.loading = false
|
||||
vnode.state.error = ''
|
||||
this.loading = false
|
||||
this.preview = null
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
|
@ -41,27 +35,28 @@ const FileUpload = {
|
|||
return m('fileupload', {
|
||||
class: vnode.attrs.class || null,
|
||||
}, [
|
||||
m('div.error', {
|
||||
hidden: !vnode.state.error,
|
||||
}, vnode.state.error),
|
||||
(media
|
||||
this.preview || media
|
||||
? vnode.attrs.useimg
|
||||
? [ m('img', { src: media.large_url }), m('div.showicon')]
|
||||
? [ m('img', { src: this.preview && this.preview.preview || media.large_url }), m('div.showicon')]
|
||||
: m('a.display.inside', {
|
||||
href: media.large_url,
|
||||
href: this.preview && this.preview.preview || media.large_url,
|
||||
style: {
|
||||
'background-image': 'url("' + media.large_url + '")',
|
||||
'background-image': 'url("' + (this.preview && this.preview.preview || media.large_url) + '")',
|
||||
},
|
||||
}, m('div.showicon'))
|
||||
: m('div.inside.showbordericon')
|
||||
),
|
||||
,
|
||||
m('input', {
|
||||
accept: 'image/*',
|
||||
type: 'file',
|
||||
onchange: this.uploadFile.bind(this, vnode),
|
||||
onchange: this.fileChanged.bind(this, vnode),
|
||||
}),
|
||||
(media && vnode.attrs.ondelete ? m('button.remove', { onclick: vnode.attrs.ondelete }) : null),
|
||||
(vnode.state.loading ? m('div.loading-spinner') : null),
|
||||
media && vnode.attrs.ondelete
|
||||
? m('button.remove', { onclick: vnode.attrs.ondelete })
|
||||
: null,
|
||||
this.loading
|
||||
? m('div.loading-spinner')
|
||||
: null,
|
||||
])
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@ const Fileinfo = require('./fileinfo')
|
|||
|
||||
const Newsitem = {
|
||||
oninit: function(vnode) {
|
||||
if (vnode.attrs.media) {
|
||||
this.srcsetJpeg = vnode.attrs.media.small_url + ' 500w, '
|
||||
+ vnode.attrs.media.medium_url + ' 800w '
|
||||
if (vnode.attrs.media.small_url_avif) {
|
||||
this.srcsetAvif = vnode.attrs.media.small_url_avif + ' 500w, '
|
||||
+ vnode.attrs.media.medium_url_avif + ' 800w '
|
||||
if (vnode.attrs.article.media) {
|
||||
this.srcsetJpeg = vnode.attrs.article.media.small_url + ' 500w, '
|
||||
+ vnode.attrs.article.media.medium_url + ' 800w '
|
||||
if (vnode.attrs.article.media.small_url_avif) {
|
||||
this.srcsetAvif = vnode.attrs.article.media.small_url_avif + ' 500w, '
|
||||
+ vnode.attrs.article.media.medium_url_avif + ' 800w '
|
||||
} else {
|
||||
this.srcsetAvif = null
|
||||
}
|
||||
|
@ -20,14 +20,14 @@ const Newsitem = {
|
|||
view: function(vnode) {
|
||||
return m('newsitem', [
|
||||
m(m.route.Link,
|
||||
{ href: '/article/' + vnode.attrs.path, class: 'title' },
|
||||
m('h3', [vnode.attrs.name])
|
||||
{ href: '/article/' + vnode.attrs.article.path, class: 'title' },
|
||||
m('h3', [vnode.attrs.article.name])
|
||||
),
|
||||
m('div.newsitemcontent', [
|
||||
vnode.attrs.media
|
||||
vnode.attrs.article.media
|
||||
? m(m.route.Link, {
|
||||
class: 'cover',
|
||||
href: '/article/' + vnode.attrs.path,
|
||||
href: '/article/' + vnode.attrs.article.path,
|
||||
},
|
||||
m('picture', [
|
||||
this.srcsetAvif ? m('source', {
|
||||
|
@ -38,28 +38,28 @@ const Newsitem = {
|
|||
m('img', {
|
||||
srcset: this.srcsetJpeg,
|
||||
sizes: this.coverSizes,
|
||||
alt: 'Image for news item ' + vnode.attrs.name,
|
||||
src: vnode.attrs.media.small_url }),
|
||||
alt: 'Image for news item ' + vnode.attrs.article.name,
|
||||
src: vnode.attrs.article.media.small_url }),
|
||||
])
|
||||
)
|
||||
: null,
|
||||
m('div.entrycontent', {
|
||||
class: vnode.attrs.media ? '' : 'extrapadding',
|
||||
class: vnode.attrs.article.media ? '' : 'extrapadding',
|
||||
}, [
|
||||
(vnode.attrs.description
|
||||
? m('.fr-view', m.trust(vnode.attrs.description))
|
||||
(vnode.attrs.article.content
|
||||
? m('.fr-view', m.trust(vnode.attrs.article.content))
|
||||
: null),
|
||||
(vnode.attrs.files && vnode.attrs.files.length
|
||||
? vnode.attrs.files.map(function(file) {
|
||||
(vnode.attrs.article.files && vnode.attrs.article.files.length
|
||||
? vnode.attrs.article.files.map(function(file) {
|
||||
return m(Fileinfo, { file: file, trim: true })
|
||||
})
|
||||
: null),
|
||||
m('span.entrymeta', [
|
||||
'Posted ',
|
||||
(vnode.attrs.parent ? 'in' : ''),
|
||||
(vnode.attrs.parent ? m(m.route.Link, { href: '/page/' + vnode.attrs.parent.path }, vnode.attrs.parent.name) : null),
|
||||
'at ' + (vnode.attrs.published_at.replace('T', ' ').split('.')[0]).substr(0, 16),
|
||||
' by ' + (vnode.attrs.staff && vnode.attrs.staff.fullname || 'Admin'),
|
||||
(vnode.attrs.article.page_path ? 'in' : ''),
|
||||
(vnode.attrs.article.page_path ? m(m.route.Link, { href: '/page/' + vnode.attrs.article.page_path }, vnode.attrs.article.page_name) : null),
|
||||
'at ' + (vnode.attrs.article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16),
|
||||
' by ' + (vnode.attrs.article.admin_name || 'Admin'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
|
|
@ -1,35 +1,41 @@
|
|||
const Pages = {
|
||||
oninit: function(vnode) {
|
||||
this.onpage = vnode.attrs.onpage || function() {}
|
||||
this.onbeforeupdate(vnode)
|
||||
},
|
||||
|
||||
onbeforeupdate: function(vnode) {
|
||||
this.total = vnode.attrs.total
|
||||
this.currentPage = vnode.attrs.page
|
||||
this.perPage = vnode.attrs.perPage || 10
|
||||
this.maxPage = this.total / this.perPage + 1
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
if (!vnode.attrs.links) return null
|
||||
if (this.total <= this.perPage) return null
|
||||
return m('pages', [
|
||||
vnode.attrs.links.first
|
||||
? m(m.route.Link, {
|
||||
href: vnode.attrs.base + '?page=' + vnode.attrs.links.first.page,
|
||||
onclick: function() { vnode.state.onpage(vnode.attrs.links.first.page) },
|
||||
}, 'First')
|
||||
this.currentPage > 1
|
||||
? [
|
||||
m(m.route.Link, {
|
||||
href: vnode.attrs.base,
|
||||
}, 'First'),
|
||||
m(m.route.Link, {
|
||||
href: vnode.attrs.base + (this.currentPage > 2
|
||||
? '?page=' + (this.currentPage - 1)
|
||||
: ''
|
||||
),
|
||||
}, 'Previous'),
|
||||
]
|
||||
: m('div'),
|
||||
vnode.attrs.links.previous
|
||||
? m(m.route.Link, {
|
||||
href: vnode.attrs.base + '?page=' + vnode.attrs.links.previous.page,
|
||||
onclick: function() { vnode.state.onpage(vnode.attrs.links.previous.page) },
|
||||
}, vnode.attrs.links.previous.title)
|
||||
: m('div'),
|
||||
m('div', vnode.attrs.links.current && vnode.attrs.links.current.title || 'Current page'),
|
||||
vnode.attrs.links.next
|
||||
? m(m.route.Link, {
|
||||
href: vnode.attrs.base + '?page=' + vnode.attrs.links.next.page,
|
||||
onclick: function() { vnode.state.onpage(vnode.attrs.links.next.page) },
|
||||
}, vnode.attrs.links.next.title)
|
||||
: m('div'),
|
||||
vnode.attrs.links.last
|
||||
? m(m.route.Link, {
|
||||
href: vnode.attrs.base + '?page=' + vnode.attrs.links.last.page,
|
||||
onclick: function() { vnode.state.onpage(vnode.attrs.links.last.page) },
|
||||
m('div', 'Page ' + this.currentPage),
|
||||
this.currentPage < this.maxPage
|
||||
? [
|
||||
m(m.route.Link, {
|
||||
href: vnode.attrs.base + '?page=' + (this.currentPage + 1),
|
||||
}, 'Next'),
|
||||
m(m.route.Link, {
|
||||
href: vnode.attrs.base + '?page=' + this.maxPage,
|
||||
}, 'Last')
|
||||
]
|
||||
: m('div'),
|
||||
])
|
||||
},
|
||||
|
|
|
@ -49,8 +49,9 @@
|
|||
"dependencies": {
|
||||
"bencode": "^2.0.3",
|
||||
"dot": "^2.0.0-beta.1",
|
||||
"flaska": "^1.2.5",
|
||||
"flaska": "^1.3.0",
|
||||
"format-link-header": "^2.1.0",
|
||||
"formidable": "^1.2.6",
|
||||
"msnodesqlv8": "^2.4.7",
|
||||
"nconf-lite": "^1.0.1",
|
||||
"striptags": "^3.1.1"
|
||||
|
|
2
public/assets/editor.js
Normal file
2
public/assets/editor.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -33,6 +33,6 @@
|
|||
<main id="main"></main>
|
||||
<footer id="footer"></footer>
|
||||
</div>
|
||||
<script type="text/javascript" src="/assets/app.js?v={{=version}}" nonce="{{=nonce}}"></script>
|
||||
<script type="text/javascript" src="/assets/app.js?v={{=version}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue