Compare commits
3 commits
4014783dbf
...
95e3737e91
Author | SHA1 | Date | |
---|---|---|---|
95e3737e91 | |||
296bf51c9a | |||
eb9ffde724 |
135 changed files with 4509 additions and 512 deletions
1
base/.npmrc
Normal file
1
base/.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package-lock=false
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { FormidableHandler } from 'flaska'
|
||||||
import { parseFiles } from '../file/util.mjs'
|
import { parseFiles } from '../file/util.mjs'
|
||||||
import { parseArticles, parseArticle } from './util.mjs'
|
import { parseArticles, parseArticle } from './util.mjs'
|
||||||
import { upload } from '../media/upload.mjs'
|
import { upload } from '../media/upload.mjs'
|
||||||
|
@ -10,6 +11,17 @@ export default class ArticleRoutes {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
server.flaska.get('/api/articles/:path', this.getArticle.bind(this))
|
||||||
|
server.flaska.get('/api/auth/articles', server.authenticate(), this.auth_getAllArticles.bind(this))
|
||||||
|
server.flaska.get('/api/auth/articles/:id', server.authenticate(), this.auth_getSingleArticle.bind(this))
|
||||||
|
server.flaska.put('/api/auth/articles/:id', [
|
||||||
|
server.authenticate(),
|
||||||
|
server.formidable({ maxFileSize: 20 * 1024 * 1024, }),
|
||||||
|
], this.auth_updateCreateSingleArticle.bind(this))
|
||||||
|
server.flaska.delete('/api/auth/articles/:id', server.authenticate(), this.auth_removeSingleArticle.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
/** GET: /api/articles/[path] */
|
/** GET: /api/articles/[path] */
|
||||||
async getArticle(ctx) {
|
async getArticle(ctx) {
|
||||||
let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path])
|
let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path])
|
||||||
|
@ -27,12 +39,12 @@ export default class ArticleRoutes {
|
||||||
let res = await ctx.db.safeCallProc('article_auth_get_all', [
|
let res = await ctx.db.safeCallProc('article_auth_get_all', [
|
||||||
ctx.state.auth_token,
|
ctx.state.auth_token,
|
||||||
Math.max(ctx.query.get('page') || 1, 1),
|
Math.max(ctx.query.get('page') || 1, 1),
|
||||||
Math.min(ctx.query.get('per_page') || 10, 25)
|
Math.min(ctx.query.get('per_page') || 20, 100)
|
||||||
])
|
])
|
||||||
|
|
||||||
let out = {
|
let out = {
|
||||||
articles: parseArticles(res.results[0]),
|
articles: parseArticles(res.results[0]),
|
||||||
total_articles: res.results[0][0].total_articles,
|
total_articles: res.results[1][0].total_articles,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = out
|
ctx.body = out
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { parseFile } from '../file/util.mjs'
|
import { parseFile } from '../file/util.mjs'
|
||||||
|
import { contentToBlocks, parseMediaAndBanner } from '../util.mjs'
|
||||||
|
|
||||||
export function parseArticles(articles) {
|
export function parseArticles(articles) {
|
||||||
for (let i = 0; i < articles.length; i++) {
|
for (let i = 0; i < articles.length; i++) {
|
||||||
|
@ -23,36 +24,7 @@ export function parseArticle(article) {
|
||||||
if (!article) {
|
if (!article) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (article.content) {
|
article.content = contentToBlocks(article.content)
|
||||||
if (article.content[0] === '{') {
|
parseMediaAndBanner(article)
|
||||||
try {
|
|
||||||
article.content = JSON.parse(article.content)
|
|
||||||
} catch (err) {
|
|
||||||
article.content = {
|
|
||||||
time: new Date().getTime(),
|
|
||||||
blocks: [
|
|
||||||
{id: '1', type: 'paragraph', data: { text: 'Error parsing article content: ' + err.message }},
|
|
||||||
],
|
|
||||||
version: '2.25.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
article.content = {
|
|
||||||
time: new Date().getTime(),
|
|
||||||
blocks: [
|
|
||||||
{id: '1', type: 'htmlraw', data: { html: article.content }},
|
|
||||||
],
|
|
||||||
version: '2.25.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (article.banner_path) {
|
|
||||||
article.banner_path = 'https://cdn.nfp.is' + article.banner_path
|
|
||||||
article.banner_alt_prefix = 'https://cdn.nfp.is' + article.banner_alt_prefix
|
|
||||||
}
|
|
||||||
if (article.media_path) {
|
|
||||||
article.media_path = 'https://cdn.nfp.is' + article.media_path
|
|
||||||
article.media_alt_prefix = 'https://cdn.nfp.is' + article.media_alt_prefix
|
|
||||||
}
|
|
||||||
return article
|
return article
|
||||||
}
|
}
|
|
@ -11,6 +11,10 @@ export default class AuthenticationRoutes {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
server.flaska.post('/api/authentication/login', server.jsonHandler(), this.login.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
/** GET: /api/authentication/login */
|
/** GET: /api/authentication/login */
|
||||||
async login(ctx) {
|
async login(ctx) {
|
||||||
let res = await ctx.db.safeCallProc('auth_login', [
|
let res = await ctx.db.safeCallProc('auth_login', [
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import nconf from 'nconf-lite'
|
import nconf from 'nconf-lite'
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
|
|
||||||
// Helper method for global usage.
|
// Helper method for global usage.
|
||||||
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
|
|
||||||
// taken from isobject npm library
|
|
||||||
function isObject(val) {
|
|
||||||
return val != null && typeof val === 'object' && Array.isArray(val) === false
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function defaults(options, def) {
|
|
||||||
let out = { }
|
|
||||||
|
|
||||||
if (options) {
|
|
||||||
Object.keys(options || {}).forEach(key => {
|
|
||||||
out[key] = options[key]
|
|
||||||
|
|
||||||
if (Array.isArray(out[key])) {
|
|
||||||
out[key] = out[key].map(item => {
|
|
||||||
if (isObject(item)) return defaults(item)
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
} else if (out[key] && typeof out[key] === 'object') {
|
|
||||||
out[key] = defaults(options[key], def && def[key])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (def) {
|
|
||||||
Object.keys(def).forEach(function(key) {
|
|
||||||
if (typeof out[key] === 'undefined') {
|
|
||||||
out[key] = def[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
9
base/package.json
Normal file
9
base/package.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"dot": "^2.0.0-beta.1",
|
||||||
|
"flaska": "^1.3.0",
|
||||||
|
"formidable": "^1.2.6",
|
||||||
|
"msnodesqlv8": "^2.4.7",
|
||||||
|
"nconf-lite": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { parsePagesToTree } from './util.mjs'
|
import { parsePage, parsePagesToTree } from './util.mjs'
|
||||||
import { upload } from '../media/upload.mjs'
|
import { upload } from '../media/upload.mjs'
|
||||||
import { combineFilesWithArticles, parseArticle, parseArticles } from '../article/util.mjs'
|
import { combineFilesWithArticles, parseArticle, parseArticles } from '../article/util.mjs'
|
||||||
import { mediaToDatabase } from '../media/util.mjs'
|
import { mediaToDatabase } from '../media/util.mjs'
|
||||||
|
@ -11,6 +11,19 @@ export default class PageRoutes {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
server.flaska.get('/api/pagetree', this.getPageTree.bind(this))
|
||||||
|
server.flaska.get('/api/frontpage', this.getPage.bind(this))
|
||||||
|
server.flaska.get('/api/pages/:path', this.getPage.bind(this))
|
||||||
|
server.flaska.get('/api/auth/pages', server.authenticate(), this.auth_getAllPages.bind(this))
|
||||||
|
server.flaska.get('/api/auth/pages/:id', server.authenticate(), this.auth_getSinglePage.bind(this))
|
||||||
|
server.flaska.put('/api/auth/pages/:id', [
|
||||||
|
server.authenticate(),
|
||||||
|
server.formidable({ maxFileSize: 20 * 1024 * 1024, }),
|
||||||
|
], this.auth_updateCreateSinglePage.bind(this))
|
||||||
|
server.flaska.delete('/api/auth/pages/:id', server.authenticate(), this.auth_removeSinglePage.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
/** GET: /api/pagetree */
|
/** GET: /api/pagetree */
|
||||||
async getPageTree(ctx, onlyReturn = false) {
|
async getPageTree(ctx, onlyReturn = false) {
|
||||||
let res = await ctx.db.safeCallProc('pages_get_tree', [])
|
let res = await ctx.db.safeCallProc('pages_get_tree', [])
|
||||||
|
@ -30,7 +43,7 @@ export default class PageRoutes {
|
||||||
])
|
])
|
||||||
|
|
||||||
let out = {
|
let out = {
|
||||||
page: res.results[0][0] || null,
|
page: parsePage(res.results[0][0]),
|
||||||
articles: parseArticles(res.results[1]),
|
articles: parseArticles(res.results[1]),
|
||||||
total_articles: res.results[2][0].total_articles,
|
total_articles: res.results[2][0].total_articles,
|
||||||
featured: parseArticle(res.results[4][0]),
|
featured: parseArticle(res.results[4][0]),
|
||||||
|
@ -70,7 +83,7 @@ export default class PageRoutes {
|
||||||
let res = await ctx.db.safeCallProc('pages_auth_get_update_create', params)
|
let res = await ctx.db.safeCallProc('pages_auth_get_update_create', params)
|
||||||
|
|
||||||
let out = {
|
let out = {
|
||||||
page: res.results[0][0] || {},
|
page: parsePage(res.results[0][0] || {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = out
|
ctx.body = out
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { contentToBlocks, parseMediaAndBanner } from '../util.mjs'
|
||||||
|
|
||||||
export function parsePagesToTree(pages) {
|
export function parsePagesToTree(pages) {
|
||||||
let out = []
|
let out = []
|
||||||
let children = []
|
let children = []
|
||||||
|
@ -21,3 +23,12 @@ export function parsePagesToTree(pages) {
|
||||||
tree: out
|
tree: out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parsePage(page) {
|
||||||
|
if (!page) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
page.content = contentToBlocks(page.content)
|
||||||
|
parseMediaAndBanner(page)
|
||||||
|
return page
|
||||||
|
}
|
|
@ -1,124 +0,0 @@
|
||||||
import _ from 'lodash'
|
|
||||||
import config from '../config.mjs'
|
|
||||||
|
|
||||||
function limit(value, min, max, fallback) {
|
|
||||||
let out = parseInt(value, 10)
|
|
||||||
|
|
||||||
if (!out) {
|
|
||||||
out = fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
if (out < min) {
|
|
||||||
out = min
|
|
||||||
}
|
|
||||||
|
|
||||||
if (out > max) {
|
|
||||||
out = max
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parsePagination(ctx) {
|
|
||||||
let out = {
|
|
||||||
perPage: limit(ctx.query.perPage, 1, 1500, 1250),
|
|
||||||
page: limit(ctx.query.page, 1, Number.MAX_SAFE_INTEGER, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(ctx.query).forEach(item => {
|
|
||||||
if (item.startsWith('perPage.')) {
|
|
||||||
let name = item.substring(8)
|
|
||||||
out[name] = {
|
|
||||||
perPage: limit(ctx.query[`perPage.${name}`], 1, 1500, 1250),
|
|
||||||
page: limit(ctx.query[`page.${name}`], 1, Number.MAX_SAFE_INTEGER, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseFilter(ctx) {
|
|
||||||
let where
|
|
||||||
let whereNot
|
|
||||||
|
|
||||||
where = _.omitBy(ctx.query, test => test[0] === '!')
|
|
||||||
|
|
||||||
whereNot = _.pickBy(ctx.query, test => test[0] === '!')
|
|
||||||
whereNot = _.transform(
|
|
||||||
whereNot,
|
|
||||||
(result, value, key) => (result[key] = value.slice(1))
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
where: pick => _.pick(where, pick),
|
|
||||||
whereNot: pick => _.pick(whereNot, pick),
|
|
||||||
includes: (ctx.query.includes && ctx.query.includes.split(',')) || [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateLinks(ctx, total) {
|
|
||||||
let out = []
|
|
||||||
|
|
||||||
let base = _(ctx.query)
|
|
||||||
.omit(['page'])
|
|
||||||
.transform((res, val, key) => res.push(`${key}=${val}`), [])
|
|
||||||
.value()
|
|
||||||
|
|
||||||
if (!ctx.query.perPage) {
|
|
||||||
base.push(`perPage=${ctx.state.pagination.perPage}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// let protocol = ctx.protocol
|
|
||||||
|
|
||||||
// if (config.get('frontend:url').startsWith('https')) {
|
|
||||||
// protocol = 'https'
|
|
||||||
// }
|
|
||||||
|
|
||||||
let proto = 'http'
|
|
||||||
|
|
||||||
if (config.get('frontend:url').startsWith('https')) {
|
|
||||||
proto = 'https'
|
|
||||||
}
|
|
||||||
|
|
||||||
let first = new URL(ctx.path, proto + '://' + ctx.host).toString()
|
|
||||||
|
|
||||||
first += `?${base.join('&')}`
|
|
||||||
|
|
||||||
// Add the current page first
|
|
||||||
out.push({
|
|
||||||
rel: 'current',
|
|
||||||
title: `Page ${ctx.query.page || 1}`,
|
|
||||||
url: `${first}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Then add any previous pages if we can
|
|
||||||
if (ctx.state.pagination.page > 1) {
|
|
||||||
out.push({
|
|
||||||
rel: 'previous',
|
|
||||||
title: 'Previous',
|
|
||||||
url: `${first}&page=${ctx.state.pagination.page - 1}`,
|
|
||||||
})
|
|
||||||
out.push({
|
|
||||||
rel: 'first',
|
|
||||||
title: 'First',
|
|
||||||
url: `${first}&page=1`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then add any next pages if we can
|
|
||||||
if ((ctx.state.pagination.perPage * (ctx.state.pagination.page - 1)) + ctx.state.pagination.perPage < total) {
|
|
||||||
out.push({
|
|
||||||
rel: 'next',
|
|
||||||
title: 'Next',
|
|
||||||
url: `${first}&page=${ctx.state.pagination.page + 1}`,
|
|
||||||
})
|
|
||||||
out.push({
|
|
||||||
rel: 'last',
|
|
||||||
title: 'Last',
|
|
||||||
url: `${first}&page=${Math.ceil(total / ctx.state.pagination.perPage)}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import format from 'format-link-header'
|
|
||||||
|
|
||||||
import * as pagination from './helpers.mjs'
|
|
||||||
|
|
||||||
export default class ParserMiddleware {
|
|
||||||
constructor(opts = {}) {
|
|
||||||
Object.assign(this, {
|
|
||||||
pagination: opts.pagination || pagination,
|
|
||||||
format: opts.format || format,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
contextParser() {
|
|
||||||
return (ctx) => {
|
|
||||||
ctx.state.pagination = this.pagination.parsePagination(ctx)
|
|
||||||
ctx.state.filter = this.pagination.parseFilter(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateLinks() {
|
|
||||||
return async (ctx, next) => {
|
|
||||||
await next()
|
|
||||||
|
|
||||||
if (ctx.state.pagination.total > 0) {
|
|
||||||
ctx.set('Link', this.format(this.pagination.generateLinks(ctx, ctx.state.pagination.total)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.state.pagination.total != null) {
|
|
||||||
ctx.set('pagination_total', ctx.state.pagination.total)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -21,6 +21,10 @@ export default class ServeHandler {
|
||||||
// console.log(indexFile.toString())
|
// console.log(indexFile.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
server.flaska.get('/::file', this.serve.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
/** GET: /::file */
|
/** GET: /::file */
|
||||||
serve(ctx) {
|
serve(ctx) {
|
||||||
if (ctx.params.file.startsWith('api/')) {
|
if (ctx.params.file.startsWith('api/')) {
|
||||||
|
|
110
base/server.mjs
110
base/server.mjs
|
@ -4,39 +4,70 @@ import formidable from 'formidable'
|
||||||
import { initPool } from './db.mjs'
|
import { initPool } from './db.mjs'
|
||||||
import config from './config.mjs'
|
import config from './config.mjs'
|
||||||
import PageRoutes from './page/routes.mjs'
|
import PageRoutes from './page/routes.mjs'
|
||||||
import ServeHandler from './serve.mjs'
|
|
||||||
import ArticleRoutes from './article/routes.mjs'
|
import ArticleRoutes from './article/routes.mjs'
|
||||||
import AuthenticationRoutes from './authentication/routes.mjs'
|
import AuthenticationRoutes from './authentication/routes.mjs'
|
||||||
import { authenticate } from './authentication/security.mjs'
|
import { authenticate } from './authentication/security.mjs'
|
||||||
|
|
||||||
export function run(http, port, core) {
|
export default class Server {
|
||||||
let localUtil = new core.sc.Util(import.meta.url)
|
constructor(http, port, core, opts = {}) {
|
||||||
|
Object.assign(this, opts)
|
||||||
|
this.http = http
|
||||||
|
this.port = port
|
||||||
|
this.core = core
|
||||||
|
|
||||||
// Create our server
|
this.flaskaOptions = {
|
||||||
const flaska = new Flaska({
|
|
||||||
appendHeaders: {
|
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'`,
|
'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,
|
log: this.core.log,
|
||||||
nonce: ['script-src'],
|
nonce: ['script-src'],
|
||||||
nonceCacheLength: 50,
|
nonceCacheLength: 50,
|
||||||
}, http)
|
}
|
||||||
|
this.authenticate = authenticate
|
||||||
|
this.formidable = FormidableHandler.bind(this, formidable)
|
||||||
|
this.jsonHandler = JsonHandler
|
||||||
|
this.routes = [
|
||||||
|
new PageRoutes(),
|
||||||
|
new ArticleRoutes(),
|
||||||
|
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)
|
||||||
|
|
||||||
// Create our database pool
|
// Create our database pool
|
||||||
let pool = initPool(core, config.get('mssql'))
|
let pool = this.runCreateDatabase()
|
||||||
|
|
||||||
// configure our server
|
// configure our server
|
||||||
if (config.get('NODE_ENV') === 'development') {
|
if (config.get('NODE_ENV') === 'development') {
|
||||||
flaska.devMode()
|
this.flaska.devMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
flaska.before(function(ctx) {
|
this.flaska.before(function(ctx) {
|
||||||
ctx.state.started = new Date().getTime()
|
ctx.state.started = new Date().getTime()
|
||||||
ctx.db = pool
|
ctx.db = pool
|
||||||
})
|
})
|
||||||
flaska.before(QueryHandler())
|
this.flaska.before(QueryHandler())
|
||||||
|
|
||||||
flaska.after(function(ctx) {
|
this.flaska.after(function(ctx) {
|
||||||
let ended = new Date().getTime()
|
let ended = new Date().getTime()
|
||||||
var requestTime = ended - ctx.state.started
|
var requestTime = ended - ctx.state.started
|
||||||
|
|
||||||
|
@ -55,42 +86,29 @@ export function run(http, port, core) {
|
||||||
status: ctx.status,
|
status: ctx.status,
|
||||||
}, `<-- ${status}${ctx.method} ${ctx.url}`)
|
}, `<-- ${status}${ctx.method} ${ctx.url}`)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const page = new PageRoutes()
|
runRegisterRoutes() {
|
||||||
flaska.get('/api/pagetree', page.getPageTree.bind(page))
|
for (let route of this.routes) {
|
||||||
flaska.get('/api/frontpage', page.getPage.bind(page))
|
route.register(this)
|
||||||
flaska.get('/api/pages/:path', page.getPage.bind(page))
|
}
|
||||||
flaska.get('/api/auth/pages', authenticate(), page.auth_getAllPages.bind(page))
|
}
|
||||||
flaska.get('/api/auth/pages/:id', authenticate(), page.auth_getSinglePage.bind(page))
|
|
||||||
flaska.put('/api/auth/pages/:id', [
|
|
||||||
authenticate(),
|
|
||||||
FormidableHandler(formidable, { maxFileSize: 20 * 1024 * 1024, }),
|
|
||||||
], page.auth_updateCreateSinglePage.bind(page))
|
|
||||||
flaska.delete('/api/auth/pages/:id', authenticate(), page.auth_removeSinglePage.bind(page))
|
|
||||||
|
|
||||||
|
runCreateDatabase() {
|
||||||
|
return initPool(this.core, config.get('mssql'))
|
||||||
|
}
|
||||||
|
|
||||||
const article = new ArticleRoutes()
|
runStartListen() {
|
||||||
flaska.get('/api/articles/:path', article.getArticle.bind(article))
|
return this.flaska.listenAsync(this.port).then(() => {
|
||||||
flaska.get('/api/auth/articles', authenticate(), article.auth_getAllArticles.bind(article))
|
this.core.log.info('Server is listening on port ' + this.port)
|
||||||
flaska.get('/api/auth/articles/:id', authenticate(), article.auth_getSingleArticle.bind(article))
|
|
||||||
flaska.put('/api/auth/articles/:id', [
|
|
||||||
authenticate(),
|
|
||||||
FormidableHandler(formidable, { maxFileSize: 20 * 1024 * 1024, }),
|
|
||||||
], article.auth_updateCreateSingleArticle.bind(article))
|
|
||||||
flaska.delete('/api/auth/articles/:id', authenticate(), article.auth_removeSingleArticle.bind(article))
|
|
||||||
|
|
||||||
const authentication = new AuthenticationRoutes()
|
|
||||||
flaska.post('/api/authentication/login', JsonHandler(), authentication.login.bind(authentication))
|
|
||||||
|
|
||||||
const serve = new ServeHandler({
|
|
||||||
pageRoutes: page,
|
|
||||||
root: localUtil.getPathFromRoot('../public'),
|
|
||||||
version: core.app.running,
|
|
||||||
frontend: config.get('frontend:url'),
|
|
||||||
})
|
|
||||||
flaska.get('/::file', serve.serve.bind(serve))
|
|
||||||
|
|
||||||
return flaska.listenAsync(port).then(function() {
|
|
||||||
core.log.info('Server is listening on port ' + port)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
this.addCustomRoutes()
|
||||||
|
this.runCreateServer()
|
||||||
|
this.runRegisterRoutes()
|
||||||
|
|
||||||
|
return this.runStartListen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,3 +19,40 @@ export function decode(base64StringUrlSafe) {
|
||||||
}
|
}
|
||||||
return Buffer.from(base64String, 'base64')
|
return Buffer.from(base64String, 'base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseMediaAndBanner(item) {
|
||||||
|
if (item.banner_path) {
|
||||||
|
item.banner_path = 'https://cdn.nfp.is' + item.banner_path
|
||||||
|
item.banner_alt_prefix = 'https://cdn.nfp.is' + item.banner_alt_prefix
|
||||||
|
}
|
||||||
|
if (item.media_path) {
|
||||||
|
item.media_path = 'https://cdn.nfp.is' + item.media_path
|
||||||
|
item.media_alt_prefix = 'https://cdn.nfp.is' + item.media_alt_prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contentToBlocks(content) {
|
||||||
|
if (!content) return content
|
||||||
|
|
||||||
|
if (content[0] === '{') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
time: new Date().getTime(),
|
||||||
|
blocks: [
|
||||||
|
{id: '1', type: 'paragraph', data: { text: 'Error parsing content: ' + err.message }},
|
||||||
|
],
|
||||||
|
version: '2.25.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
time: new Date().getTime(),
|
||||||
|
blocks: [
|
||||||
|
{id: '1', type: 'htmlraw', data: { html: content }},
|
||||||
|
],
|
||||||
|
version: '2.25.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1
nfp_moe/.npmrc
Normal file
1
nfp_moe/.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package-lock=false
|
22
nfp_moe/api/server.mjs
Normal file
22
nfp_moe/api/server.mjs
Normal file
|
@ -0,0 +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'
|
||||||
|
|
||||||
|
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'; iframe-src talk.hyvor.com` //; frame-ancestors 'none'`
|
||||||
|
}
|
||||||
|
|
||||||
|
addCustomRoutes() {
|
||||||
|
let page = this.getRouteInstance(PageRoutes)
|
||||||
|
|
||||||
|
let localUtil = new this.core.sc.Util(import.meta.url)
|
||||||
|
this.routes.push(new ServeHandler({
|
||||||
|
pageRoutes: page,
|
||||||
|
root: localUtil.getPathFromRoot('../public'),
|
||||||
|
version: this.core.app.running,
|
||||||
|
frontend: config.get('frontend:url'),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
require('./dtsel')
|
require('./dtsel')
|
||||||
const FileUpload = require('../widgets/fileupload')
|
const FileUpload = require('./fileupload')
|
||||||
const Page = require('../api/page')
|
const PageTree = require('../page_tree')
|
||||||
const Fileinfo = require('../widgets/fileinfo')
|
const Fileinfo = require('../fileinfo')
|
||||||
const common = require('../api/common')
|
const api = require('../api')
|
||||||
const Editor = require('./editor')
|
const Editor = require('./editor')
|
||||||
|
|
||||||
const EditArticle = {
|
const EditArticle = {
|
||||||
|
@ -16,7 +16,7 @@ const EditArticle = {
|
||||||
staff: [],
|
staff: [],
|
||||||
}
|
}
|
||||||
this.pages = [{id: null, name: 'Frontpage'}]
|
this.pages = [{id: null, name: 'Frontpage'}]
|
||||||
this.pages = this.pages.concat(Page.getFlatTree())
|
this.pages = this.pages.concat(PageTree.getFlatTree())
|
||||||
this.newBanner = null
|
this.newBanner = null
|
||||||
this.newMedia = null
|
this.newMedia = null
|
||||||
this.dateInstance = null
|
this.dateInstance = null
|
||||||
|
@ -35,10 +35,11 @@ const EditArticle = {
|
||||||
this.lastid = m.route.param('id')
|
this.lastid = m.route.param('id')
|
||||||
|
|
||||||
return this.requestArticle(
|
return this.requestArticle(
|
||||||
common.sendRequest({
|
api.sendRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
|
url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
requestArticle: function(data) {
|
requestArticle: function(data) {
|
||||||
|
@ -152,7 +153,7 @@ const EditArticle = {
|
||||||
.then(body => {
|
.then(body => {
|
||||||
formData.append('content', JSON.stringify(body))
|
formData.append('content', JSON.stringify(body))
|
||||||
|
|
||||||
return common.sendRequest({
|
return api.sendRequest({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
|
url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
const FileUpload = require('../widgets/fileupload')
|
const FileUpload = require('./fileupload')
|
||||||
const Page = require('../api/page.p')
|
const PageTree = require('../page_tree')
|
||||||
|
|
||||||
const common = require('../api/common')
|
const api = require('../api')
|
||||||
const Editor = require('./editor')
|
const Editor = require('./editor')
|
||||||
|
|
||||||
const EditPage = {
|
const EditPage = {
|
||||||
|
@ -12,7 +12,7 @@ const EditPage = {
|
||||||
page: null,
|
page: null,
|
||||||
}
|
}
|
||||||
this.pages = [{id: null, name: 'Frontpage'}]
|
this.pages = [{id: null, name: 'Frontpage'}]
|
||||||
this.pages = this.pages.concat(Page.getFlatTree())
|
this.pages = this.pages.concat(PageTree.getFlatTree())
|
||||||
|
|
||||||
this.newBanner = null
|
this.newBanner = null
|
||||||
this.newMedia = null
|
this.newMedia = null
|
||||||
|
@ -31,10 +31,11 @@ const EditPage = {
|
||||||
this.lastid = m.route.param('id')
|
this.lastid = m.route.param('id')
|
||||||
|
|
||||||
return this.requestPage(
|
return this.requestPage(
|
||||||
common.sendRequest({
|
api.sendRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
|
url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
requestPage: function(data) {
|
requestPage: function(data) {
|
||||||
|
@ -135,7 +136,7 @@ const EditPage = {
|
||||||
.then(body => {
|
.then(body => {
|
||||||
formData.append('content', JSON.stringify(body))
|
formData.append('content', JSON.stringify(body))
|
||||||
|
|
||||||
return common.sendRequest({
|
return api.sendRequest({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
|
url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
|
||||||
body: formData,
|
body: formData,
|
||||||
|
@ -148,9 +149,9 @@ const EditPage = {
|
||||||
this.lastid = data.page.id.toString()
|
this.lastid = data.page.id.toString()
|
||||||
m.route.set('/admin/pages/' + data.page.id)
|
m.route.set('/admin/pages/' + data.page.id)
|
||||||
}
|
}
|
||||||
return Page.refreshTree().then(() => {
|
return PageTree.refreshTree().then(() => {
|
||||||
this.pages = [{id: null, name: 'Frontpage'}]
|
this.pages = [{id: null, name: 'Frontpage'}]
|
||||||
this.pages = this.pages.concat(Page.getFlatTree())
|
this.pages = this.pages.concat(PageTree.getFlatTree())
|
||||||
|
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
const Staff = require('../api/staff')
|
|
||||||
|
|
||||||
const EditStaff = {
|
const EditStaff = {
|
||||||
|
/*
|
||||||
oninit: function(vnode) {
|
oninit: function(vnode) {
|
||||||
this.fetchStaff(vnode)
|
this.fetchStaff(vnode)
|
||||||
},
|
},
|
||||||
|
@ -146,7 +145,7 @@ const EditStaff = {
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
},
|
},*/
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = EditStaff
|
module.exports = EditStaff
|
||||||
|
|
164
nfp_moe/app/admin/site_articles.js
Normal file
164
nfp_moe/app/admin/site_articles.js
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
const Dialogue = require('./dialogue')
|
||||||
|
const api = require('../api')
|
||||||
|
const Paginator = require('../paginator')
|
||||||
|
|
||||||
|
const ItemsPerPage = 20
|
||||||
|
|
||||||
|
const AdminArticles = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.error = ''
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
|
||||||
|
onbeforeupdate: function(vnode) {
|
||||||
|
this.currentPage = Number(m.route.param('page')) || 1
|
||||||
|
|
||||||
|
if (this.currentPage !== this.lastpage) {
|
||||||
|
this.fetchArticles(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchArticles: function(vnode) {
|
||||||
|
this.error = ''
|
||||||
|
this.lastpage = this.currentPage
|
||||||
|
|
||||||
|
document.title = 'Articles Page ' + this.lastpage + ' - Admin NFP Moe'
|
||||||
|
|
||||||
|
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 api.sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/auth/articles?page=' + (this.lastpage || 1),
|
||||||
|
})
|
||||||
|
.then((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'
|
||||||
|
})
|
||||||
|
}, (err) => {
|
||||||
|
this.error = err.message
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
clearTimeout(this.showLoading)
|
||||||
|
this.showLoading = null
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmRemoveArticle: function(vnode) {
|
||||||
|
let removingArticle = this.removeArticle
|
||||||
|
this.removeArticle = null
|
||||||
|
this.loading = true
|
||||||
|
m.redraw()
|
||||||
|
|
||||||
|
return common.sendRequest({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: '/api/auth/articles/' + removingArticle.id,
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
() => this.fetchArticles(vnode),
|
||||||
|
(err) => {
|
||||||
|
this.error = err.message
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
drawArticle: function(vnode, article) {
|
||||||
|
return [
|
||||||
|
m('tr', {
|
||||||
|
class: article.hidden
|
||||||
|
? rowhidden
|
||||||
|
: article.is_featured
|
||||||
|
? 'rowfeatured'
|
||||||
|
: ''
|
||||||
|
}, [
|
||||||
|
m('td', m(m.route.Link, { href: '/admin/articles/' + article.id }, article.name)),
|
||||||
|
m('td', m(m.route.Link, { href: '/article/' + article.path }, 'View')),
|
||||||
|
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')),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return [
|
||||||
|
m('div.wrapper.admin', [
|
||||||
|
m('div.inside', [
|
||||||
|
m('h2', 'All articles'),
|
||||||
|
m('div.actions', [
|
||||||
|
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(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 },
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AdminArticles
|
117
nfp_moe/app/admin/site_pages.js
Normal file
117
nfp_moe/app/admin/site_pages.js
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
const PageTree = require('../page_tree')
|
||||||
|
const Dialogue = require('./dialogue')
|
||||||
|
const api = require('../api')
|
||||||
|
|
||||||
|
const AdminPages = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.error = ''
|
||||||
|
this.pages = []
|
||||||
|
this.removePage = null
|
||||||
|
|
||||||
|
document.title = 'Pages - Admin NFP Moe'
|
||||||
|
this.fetchPages(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchPages: function(vnode) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
return api.sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/auth/pages',
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.pages = result.tree
|
||||||
|
}, (err) => {
|
||||||
|
this.error = err.message
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmRemovePage: function(vnode) {
|
||||||
|
let removingPage = this.removePage
|
||||||
|
this.removePage = null
|
||||||
|
this.loading = true
|
||||||
|
m.redraw()
|
||||||
|
|
||||||
|
return api.sendRequest({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: '/api/auth/pages/' + removingPage.id,
|
||||||
|
})
|
||||||
|
.then(() => PageTree.refreshTree())
|
||||||
|
.then(
|
||||||
|
() => this.fetchPages(vnode),
|
||||||
|
(err) => {
|
||||||
|
this.error = err.message
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
drawPage: function(vnode, page) {
|
||||||
|
return [
|
||||||
|
m('tr', [
|
||||||
|
m('td', [
|
||||||
|
page.parent_id ? m('span.subpage', ' - ') : null,
|
||||||
|
m(m.route.Link, { href: '/admin/pages/' + page.id }, page.name),
|
||||||
|
]),
|
||||||
|
m('td', m(m.route.Link, { href: '/page/' + page.path }, '/page/' + page.path)),
|
||||||
|
m('td.right', page.updated_at.replace('T', ' ').split('.')[0]),
|
||||||
|
m('td.right', m('button', { onclick: function() { vnode.state.removePage = page } }, 'Remove')),
|
||||||
|
]),
|
||||||
|
].concat(page.children ? page.children.map(AdminPages.drawPage.bind(this, vnode)) : [])
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return [
|
||||||
|
m('div.wrapper.admin', [
|
||||||
|
m('div.inside', [
|
||||||
|
m('h2', 'All pages'),
|
||||||
|
m('div.actions', [
|
||||||
|
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.full')
|
||||||
|
: 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(Dialogue, {
|
||||||
|
hidden: vnode.state.removePage === null,
|
||||||
|
title: 'Delete ' + (vnode.state.removePage ? vnode.state.removePage.name : ''),
|
||||||
|
message: 'Are you sure you want to remove "' + (vnode.state.removePage ? vnode.state.removePage.name : '') + '" (' + (vnode.state.removePage ? vnode.state.removePage.path : '') + ')',
|
||||||
|
yes: 'Remove',
|
||||||
|
yesclass: 'alert',
|
||||||
|
no: 'Cancel',
|
||||||
|
noclass: 'cancel',
|
||||||
|
onyes: this.confirmRemovePage.bind(this, vnode),
|
||||||
|
onno: function() { vnode.state.removePage = null },
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AdminPages
|
|
@ -1,8 +1,8 @@
|
||||||
const Staff = require('../api/staff')
|
const Dialogue = require('./dialogue')
|
||||||
const Dialogue = require('../widgets/dialogue')
|
const Pages = require('../paginator')
|
||||||
const Pages = require('../widgets/pages')
|
|
||||||
|
|
||||||
const AdminStaffList = {
|
const AdminStaffList = {
|
||||||
|
/*
|
||||||
oninit: function(vnode) {
|
oninit: function(vnode) {
|
||||||
this.error = ''
|
this.error = ''
|
||||||
this.lastpage = m.route.param('page') || '1'
|
this.lastpage = m.route.param('page') || '1'
|
||||||
|
@ -104,7 +104,7 @@ const AdminStaffList = {
|
||||||
onno: function() { vnode.state.removeStaff = null },
|
onno: function() { vnode.state.removeStaff = null },
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
},
|
},*/
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = AdminStaffList
|
module.exports = AdminStaffList
|
||||||
|
|
83
nfp_moe/app/admin_loader.js
Normal file
83
nfp_moe/app/admin_loader.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
|
||||||
|
window.adminRoutes = {}
|
||||||
|
|
||||||
|
let loadingAdmin = false
|
||||||
|
let loadedAdmin = false
|
||||||
|
let loaded = 0
|
||||||
|
let elements = []
|
||||||
|
|
||||||
|
const onLoaded = function() {
|
||||||
|
loaded++
|
||||||
|
if (loaded < 2) return
|
||||||
|
|
||||||
|
Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.rank >= 10)
|
||||||
|
loadedAdmin = true
|
||||||
|
m.route.set(m.route.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onError = function(a, b, c) {
|
||||||
|
Authentication.clearToken()
|
||||||
|
elements.forEach(function(x) { x.remove() })
|
||||||
|
loadedAdmin = loadingAdmin = false
|
||||||
|
loaded = 0
|
||||||
|
m.route.set('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAdmin = function(user) {
|
||||||
|
if (loadingAdmin) {
|
||||||
|
if (loadedAdmin) {
|
||||||
|
Authentication.setAdmin(user && user.rank >= 10)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!user || user.rank < 10) return
|
||||||
|
|
||||||
|
loadingAdmin = true
|
||||||
|
|
||||||
|
let token = Authentication.getToken()
|
||||||
|
let element = document.createElement('link')
|
||||||
|
elements.push(element)
|
||||||
|
element.setAttribute('rel', 'stylesheet')
|
||||||
|
element.setAttribute('type', 'text/css')
|
||||||
|
element.setAttribute('href', '/assets/admin.css?token=' + token)
|
||||||
|
element.onload = onLoaded
|
||||||
|
element.onerror = onError
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(element)
|
||||||
|
|
||||||
|
element = document.createElement('script')
|
||||||
|
elements.push(element)
|
||||||
|
element.setAttribute('type', 'text/javascript')
|
||||||
|
element.setAttribute('src', '/assets/admin.js?token=' + token)
|
||||||
|
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)
|
||||||
|
if (Authentication.currentUser) {
|
||||||
|
loadAdmin(Authentication.currentUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Loader = {
|
||||||
|
view: function() { return m('div.loading-spinner') },
|
||||||
|
}
|
||||||
|
const AdminResolver = {
|
||||||
|
onmatch: function(args, requestedPath) {
|
||||||
|
if (window.adminRoutes[args.path]) {
|
||||||
|
return window.adminRoutes[args.path][args.id && 1 || 0]
|
||||||
|
}
|
||||||
|
return Loader
|
||||||
|
},
|
||||||
|
render: function(vnode) { return vnode },
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AdminResolver
|
53
nfp_moe/app/api.js
Normal file
53
nfp_moe/app/api.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
|
||||||
|
exports.sendRequest = function(options, isPagination) {
|
||||||
|
let token = Authentication.getToken()
|
||||||
|
let pagination = isPagination
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
options.headers = options.headers || {}
|
||||||
|
options.headers['Authorization'] = 'Bearer ' + token
|
||||||
|
}
|
||||||
|
|
||||||
|
options.extract = function(xhr) {
|
||||||
|
if (xhr.responseText && xhr.responseText.slice(0, 9) === '<!doctype') {
|
||||||
|
throw new Error('Expected JSON but got HTML (' + xhr.status + ': ' + this.url.split('?')[0] + ')')
|
||||||
|
}
|
||||||
|
let out = null
|
||||||
|
if (pagination && xhr.status < 300) {
|
||||||
|
let headers = {}
|
||||||
|
|
||||||
|
xhr.getAllResponseHeaders().split('\r\n').forEach(function(item) {
|
||||||
|
var splitted = item.split(': ')
|
||||||
|
headers[splitted[0]] = splitted[1]
|
||||||
|
})
|
||||||
|
|
||||||
|
out = {
|
||||||
|
headers: headers || {},
|
||||||
|
data: JSON.parse(xhr.responseText),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (xhr.responseText) {
|
||||||
|
out = JSON.parse(xhr.responseText)
|
||||||
|
} else {
|
||||||
|
out = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (xhr.status >= 300) {
|
||||||
|
throw out
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.request(options)
|
||||||
|
.catch(function (error) {
|
||||||
|
if (error.status === 403) {
|
||||||
|
Authentication.clearToken()
|
||||||
|
m.route.set('/login', { redirect: m.route.get() })
|
||||||
|
}
|
||||||
|
if (error.response && error.response.status) {
|
||||||
|
return Promise.reject(error.response)
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
|
}
|
96
nfp_moe/app/article.js
Normal file
96
nfp_moe/app/article.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
const Fileinfo = require('./fileinfo')
|
||||||
|
const EditorBlock = require('./editorblock')
|
||||||
|
|
||||||
|
const Article = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.lastId = null
|
||||||
|
this.onbeforeupdate(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
onbeforeupdate: function(vnode) {
|
||||||
|
let article = vnode.attrs.article
|
||||||
|
|
||||||
|
if (this.lastId !== article.id) {
|
||||||
|
this.lastId = article.id
|
||||||
|
|
||||||
|
if (article.media_alt_prefix) {
|
||||||
|
this.pictureFallback = article.media_alt_prefix + '_small.jpg'
|
||||||
|
this.pictureJpeg = article.media_alt_prefix + '_small.jpg' + ' 720w, '
|
||||||
|
+ article.media_alt_prefix + '_medium.jpg' + ' 1300w, '
|
||||||
|
+ article.media_alt_prefix + '_large.jpg 1920w'
|
||||||
|
this.pictureAvif = article.media_alt_prefix + '_small.avif' + ' 720w, '
|
||||||
|
+ article.media_alt_prefix + '_medium.avif' + ' 1300w, '
|
||||||
|
+ article.media_alt_prefix + '_large.avif 1920w'
|
||||||
|
|
||||||
|
if (vnode.attrs.full) {
|
||||||
|
this.pictureCover = '(max-width: 1280) calc(100vw - 2rem), '
|
||||||
|
+ '1248px'
|
||||||
|
} else {
|
||||||
|
this.pictureCover = '(max-width: 639px) calc(100vw - 40px), '
|
||||||
|
+ '(max-width: 1000px) 300px, '
|
||||||
|
+ '400px'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.pictureFallback = null
|
||||||
|
this.pictureJpeg = null
|
||||||
|
this.pictureAvif = null
|
||||||
|
this.pictureCover = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
let article = vnode.attrs.article
|
||||||
|
let files = vnode.attrs.files || article.files || []
|
||||||
|
|
||||||
|
return m('article', {
|
||||||
|
class: vnode.attrs.full ? 'fullsize' : '',
|
||||||
|
}, [
|
||||||
|
m(m.route.Link,
|
||||||
|
{ href: '/article/' + article.path, class: 'title' },
|
||||||
|
m('h2', article.name)
|
||||||
|
),
|
||||||
|
m('div.row', [
|
||||||
|
this.pictureFallback
|
||||||
|
? m(vnode.attrs.full ? 'a' : m.route.Link, {
|
||||||
|
class: 'cover',
|
||||||
|
target: vnode.attrs.full ? '_blank' : '',
|
||||||
|
href: vnode.attrs.full ? article.media_path : '/article/' + article.path,
|
||||||
|
},
|
||||||
|
m('picture', [
|
||||||
|
m('source', {
|
||||||
|
srcset: this.pictureAvif,
|
||||||
|
sizes: this.pictureCover,
|
||||||
|
type: 'image/avif',
|
||||||
|
}),
|
||||||
|
m('img', {
|
||||||
|
srcset: this.pictureJpeg,
|
||||||
|
sizes: this.pictureCover,
|
||||||
|
alt: 'Image for news item ' + article.name,
|
||||||
|
src: this.pictureFallback,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
m('div', [
|
||||||
|
m('div.description',
|
||||||
|
article.content.blocks.map(block => {
|
||||||
|
return m(EditorBlock, { block: block })
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
files.map(function(file) {
|
||||||
|
return m(Fileinfo, { file: file, trim: true })
|
||||||
|
}),
|
||||||
|
m('p.meta', [
|
||||||
|
'Posted ',
|
||||||
|
(article.page_path ? ['in ', m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name), ' '] : ''),
|
||||||
|
'at ' + (article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16),
|
||||||
|
' by ' + (article.admin_name || 'Admin'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Article
|
99
nfp_moe/app/article_slim.js
Normal file
99
nfp_moe/app/article_slim.js
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
const Fileinfo = require('./fileinfo')
|
||||||
|
|
||||||
|
const Articleslim = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.lastId = null
|
||||||
|
this.onbeforeupdate(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
strip: function(html) {
|
||||||
|
var doc = new DOMParser().parseFromString(html, 'text/html')
|
||||||
|
var out = (doc.body.textContent || '').replace(/([\.!?])/g, '$1 ')
|
||||||
|
var splitted = out.split('.')
|
||||||
|
if (splitted.length > 2) {
|
||||||
|
return splitted.slice(0, 2).join('.') + '...'
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
|
|
||||||
|
onbeforeupdate: function(vnode) {
|
||||||
|
let article = vnode.attrs.article
|
||||||
|
|
||||||
|
if (this.lastId !== article.id) {
|
||||||
|
this.lastId = article.id
|
||||||
|
this.description = null
|
||||||
|
|
||||||
|
if (article.content) {
|
||||||
|
for (let i = 0; i < article.content.blocks.length; i++) {
|
||||||
|
if (article.content.blocks[i].type === 'paragraph') {
|
||||||
|
this.description = article.content.blocks[i].data.text
|
||||||
|
break
|
||||||
|
} else if (article.content.blocks[i].type === 'htmlraw') {
|
||||||
|
this.description = this.strip(article.content.blocks[i].data.html)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (article.media_alt_prefix) {
|
||||||
|
this.pictureFallback = article.media_alt_prefix + '_small.jpg'
|
||||||
|
this.pictureJpeg = article.media_alt_prefix + '_small.jpg' + ' 720w, '
|
||||||
|
+ article.media_alt_prefix + '_medium.jpg' + ' 1300w, '
|
||||||
|
+ article.media_alt_prefix + '_large.jpg 1920w'
|
||||||
|
this.pictureAvif = article.media_alt_prefix + '_small.avif' + ' 720w, '
|
||||||
|
+ article.media_alt_prefix + '_medium.avif' + ' 1300w, '
|
||||||
|
+ article.media_alt_prefix + '_large.avif 1920w'
|
||||||
|
|
||||||
|
this.pictureCover = '(max-width: 440px) calc(100vw - 40px), '
|
||||||
|
+ '124px'
|
||||||
|
} else {
|
||||||
|
this.pictureFallback = null
|
||||||
|
this.pictureJpeg = null
|
||||||
|
this.pictureAvif = null
|
||||||
|
this.pictureCover = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
let article = vnode.attrs.article
|
||||||
|
|
||||||
|
return m('articleslim', [
|
||||||
|
this.pictureFallback
|
||||||
|
? m(m.route.Link, {
|
||||||
|
class: 'cover',
|
||||||
|
href: '/article/' + article.path,
|
||||||
|
},
|
||||||
|
m('picture', [
|
||||||
|
m('source', {
|
||||||
|
srcset: this.pictureAvif,
|
||||||
|
sizes: this.pictureCover,
|
||||||
|
type: 'image/avif',
|
||||||
|
}),
|
||||||
|
m('img', {
|
||||||
|
srcset: this.pictureJpeg,
|
||||||
|
sizes: this.pictureCover,
|
||||||
|
alt: 'Image for news item ' + article.name,
|
||||||
|
src: this.pictureFallback,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: m('a.cover.nobg'),
|
||||||
|
m('div', [
|
||||||
|
m(m.route.Link,
|
||||||
|
{ class: 'title', href: '/article/' + article.path },
|
||||||
|
article.name
|
||||||
|
),
|
||||||
|
(article.files && article.files.length
|
||||||
|
? article.files.map(function(file) {
|
||||||
|
return m(Fileinfo, { file: file, slim: true })
|
||||||
|
})
|
||||||
|
: this.description
|
||||||
|
? m('p.description', this.description)
|
||||||
|
: null),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Articleslim
|
|
@ -7,6 +7,7 @@ const Authentication = {
|
||||||
authListeners: [],
|
authListeners: [],
|
||||||
|
|
||||||
updateToken: function(token) {
|
updateToken: function(token) {
|
||||||
|
console.log('updateToken', token)
|
||||||
if (!token) return Authentication.clearToken()
|
if (!token) return Authentication.clearToken()
|
||||||
localStorage.setItem(storageName, token)
|
localStorage.setItem(storageName, token)
|
||||||
Authentication.currentUser = JSON.parse(atob(token.split('.')[1]))
|
Authentication.currentUser = JSON.parse(atob(token.split('.')[1]))
|
||||||
|
@ -17,6 +18,8 @@ const Authentication = {
|
||||||
},
|
},
|
||||||
|
|
||||||
clearToken: function() {
|
clearToken: function() {
|
||||||
|
var err = new Error()
|
||||||
|
console.log('clearing', err.stack)
|
||||||
Authentication.currentUser = null
|
Authentication.currentUser = null
|
||||||
localStorage.removeItem(storageName)
|
localStorage.removeItem(storageName)
|
||||||
Authentication.isAdmin = false
|
Authentication.isAdmin = false
|
||||||
|
|
92
nfp_moe/app/fileinfo.js
Normal file
92
nfp_moe/app/fileinfo.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
const Fileinfo = {
|
||||||
|
getPrefix: function(vnode) {
|
||||||
|
if (!vnode.attrs.file.filename.endsWith('.torrent')) {
|
||||||
|
return vnode.attrs.file.filename.split('.').slice(-1)
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.indexOf('720 ') >= 0) {
|
||||||
|
return '720p'
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.indexOf('1080 ') >= 0) {
|
||||||
|
return '1080p'
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.indexOf('480 ') >= 0) {
|
||||||
|
return '480p'
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.toLowerCase().indexOf('flac') >= 0) {
|
||||||
|
return 'FLAC'
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.toLowerCase().indexOf('mp3') >= 0) {
|
||||||
|
return 'MP3'
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.toLowerCase().indexOf('psd') >= 0) {
|
||||||
|
return 'PSD'
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.toLowerCase().indexOf('.zip') >= 0) {
|
||||||
|
return 'ZIP'
|
||||||
|
}
|
||||||
|
return 'Other'
|
||||||
|
},
|
||||||
|
|
||||||
|
getTitle: function(vnode) {
|
||||||
|
if (vnode.attrs.file.meta.torrent) {
|
||||||
|
return vnode.attrs.file.meta.torrent.name
|
||||||
|
}
|
||||||
|
return vnode.attrs.file.filename
|
||||||
|
},
|
||||||
|
|
||||||
|
getDownloadName: function(vnode) {
|
||||||
|
if (vnode.attrs.file.meta.torrent) {
|
||||||
|
return 'Torrent'
|
||||||
|
}
|
||||||
|
return 'Download'
|
||||||
|
},
|
||||||
|
|
||||||
|
getSize: function(orgSize) {
|
||||||
|
var size = orgSize
|
||||||
|
var i = -1
|
||||||
|
var byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
do {
|
||||||
|
size = size / 1024
|
||||||
|
i++
|
||||||
|
} while (size > 1024)
|
||||||
|
|
||||||
|
return Math.max(size, 0.1).toFixed(1) + byteUnits[i]
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return m('fileinfo', { class: vnode.attrs.slim ? 'slim' : ''}, [
|
||||||
|
m('p', [
|
||||||
|
m('span', this.getPrefix(vnode) + ':'),
|
||||||
|
m('a', {
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener',
|
||||||
|
href: vnode.attrs.file.url,
|
||||||
|
}, this.getDownloadName(vnode)),
|
||||||
|
vnode.attrs.file.magnet
|
||||||
|
? m('a', {
|
||||||
|
href: vnode.attrs.file.magnet,
|
||||||
|
}, 'Magnet')
|
||||||
|
: null,
|
||||||
|
this.getTitle(vnode),
|
||||||
|
]),
|
||||||
|
vnode.attrs.file.meta.torrent
|
||||||
|
&& !vnode.attrs.slim
|
||||||
|
&& vnode.attrs.file.meta.torrent.files.length > 1
|
||||||
|
&& (!vnode.attrs.trim || vnode.attrs.file.meta.torrent.files.length <= 4)
|
||||||
|
? m('ul', vnode.attrs.file.meta.torrent.files.map(function(file) {
|
||||||
|
return m('li', [
|
||||||
|
file.name + ' ',
|
||||||
|
m('span.meta', '(' + Fileinfo.getSize(file.size) + ')'),
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
: null,
|
||||||
|
vnode.attrs.trim
|
||||||
|
&& vnode.attrs.file.meta.torrent
|
||||||
|
&& vnode.attrs.file.meta.torrent.files.length > 4
|
||||||
|
? m('div.trimmed', '...' + vnode.attrs.file.meta.torrent.files.length + ' files...')
|
||||||
|
: null,
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Fileinfo
|
39
nfp_moe/app/footer.js
Normal file
39
nfp_moe/app/footer.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const PageTree = require('./page_tree')
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
|
||||||
|
const Footer = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.year = new Date().getFullYear()
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function() {
|
||||||
|
return [
|
||||||
|
m('span', 'Sitemap'),
|
||||||
|
m(m.route.Link, { class: 'root', href: '/' }, 'Home'),
|
||||||
|
PageTree.Tree.map(function(page) {
|
||||||
|
return [
|
||||||
|
m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name),
|
||||||
|
(page.children
|
||||||
|
? m('ul', page.children.map(function(subpage) {
|
||||||
|
return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name))
|
||||||
|
}))
|
||||||
|
: null),
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
|
||||||
|
!Authentication.currentUser
|
||||||
|
? m(m.route.Link, { class: 'root', href: '/login' }, 'Login')
|
||||||
|
: null,
|
||||||
|
m('div.meta', [
|
||||||
|
'©'
|
||||||
|
+ this.year
|
||||||
|
+ ' NFP Encodes - nfp@nfp.moe - ',
|
||||||
|
m('a', { rel: 'noopener', href: 'https://www.iubenda.com/privacy-policy/31076050', target: '_blank' }, 'Privacy Policy'),
|
||||||
|
' (Fuck EU)',
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Footer
|
115
nfp_moe/app/header.js
Normal file
115
nfp_moe/app/header.js
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
const PageTree = require('./page_tree')
|
||||||
|
|
||||||
|
const DarkModeStorageName = 'nfp_sites_darkmode'
|
||||||
|
|
||||||
|
const Menu = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.currentActive = 'home'
|
||||||
|
this.error = ''
|
||||||
|
this.loading = false
|
||||||
|
this.onbeforeupdate()
|
||||||
|
|
||||||
|
if (!PageTree.Tree.length) {
|
||||||
|
this.refreshTree()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
onbeforeupdate: function() {
|
||||||
|
this.darkIsOn = localStorage.getItem(DarkModeStorageName)
|
||||||
|
|
||||||
|
let currentPath = m.route.get()
|
||||||
|
|
||||||
|
if (currentPath === '/') this.currentActive = 'home'
|
||||||
|
else if (currentPath === '/login') this.currentActive = 'login'
|
||||||
|
else if (currentPath && currentPath.startsWith('/page')) this.currentActive = currentPath.slice(currentPath.lastIndexOf('/') + 1)
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshTree: function(vnode) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
PageTree.refreshTree()
|
||||||
|
.catch((err) => {
|
||||||
|
this.error = 'Error while getting menu tree: ' + err.message + '. Click here to try again.'
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
logOut: function() {
|
||||||
|
Authentication.clearToken()
|
||||||
|
m.route.set('/')
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleDarkMode: function() {
|
||||||
|
this.darkIsOn = !this.darkIsOn
|
||||||
|
if (this.darkIsOn) {
|
||||||
|
localStorage.setItem(DarkModeStorageName, true)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(DarkModeStorageName)
|
||||||
|
}
|
||||||
|
document.body.className = (this.darkIsOn ? 'darkmode ' : 'daymode ')
|
||||||
|
+ (window.supportsavif ? 'avifsupport' : 'jpegonly')
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function() {
|
||||||
|
return [
|
||||||
|
m('header', [
|
||||||
|
m('div.inside', [
|
||||||
|
m(m.route.Link,
|
||||||
|
{ href: '/', class: 'logo' },
|
||||||
|
m('h1', 'NFP Moe')
|
||||||
|
),
|
||||||
|
m('aside', [
|
||||||
|
Authentication.currentUser
|
||||||
|
? [
|
||||||
|
m('p', [
|
||||||
|
'Welcome ' + Authentication.currentUser.name + '. ',
|
||||||
|
m('button', { onclick: this.logOut }, '(Log out)'),
|
||||||
|
]),
|
||||||
|
m('div.actions', [
|
||||||
|
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.rank < 100, href: '/admin/staff' }, 'Staff'),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
m('button',
|
||||||
|
{ onclick: this.toggleDarkMode.bind(this) },
|
||||||
|
this.darkIsOn ? 'Day mode' : 'Night mode'
|
||||||
|
),
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m('nav', [
|
||||||
|
m('div.inside', [
|
||||||
|
m(m.route.Link, {
|
||||||
|
href: '/',
|
||||||
|
class: this.currentActive === 'home' ? 'active' : '',
|
||||||
|
}, 'Home'),
|
||||||
|
this.loading ? m('div.loading-spinner') : null,
|
||||||
|
PageTree.Tree.map((page) => {
|
||||||
|
let className = ''
|
||||||
|
if (this.currentActive === page.path) {
|
||||||
|
className += 'active '
|
||||||
|
}
|
||||||
|
return m(m.route.Link, {
|
||||||
|
href: '/page/' + page.path,
|
||||||
|
class: className,
|
||||||
|
}, page.name)
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
this.error ? m('div.error', { onclick: this.refreshTree.bind(this) }, this.error) : null,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Menu
|
|
@ -1,6 +1,11 @@
|
||||||
require('./polyfill')
|
|
||||||
|
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
const AdminResolver = require('./admin_loader')
|
||||||
|
const Header = require('./header')
|
||||||
|
const Footer = require('./footer')
|
||||||
|
const Login = require('./site_login')
|
||||||
|
const SitePage = require('./site_page')
|
||||||
|
const SiteArticle = require('./site_article')
|
||||||
window.m = m
|
window.m = m
|
||||||
|
|
||||||
m.route.setOrig = m.route.set
|
m.route.setOrig = m.route.set
|
||||||
|
@ -15,105 +20,13 @@ m.route.link = function(vnode){
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Authentication = require('./authentication')
|
|
||||||
|
|
||||||
m.route.prefix = ''
|
m.route.prefix = ''
|
||||||
window.adminRoutes = {}
|
|
||||||
let loadingAdmin = false
|
|
||||||
let loadedAdmin = false
|
|
||||||
let loaded = 0
|
|
||||||
let elements = []
|
|
||||||
|
|
||||||
const onLoaded = function() {
|
|
||||||
loaded++
|
|
||||||
if (loaded < 2) return
|
|
||||||
|
|
||||||
Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.rank >= 10)
|
|
||||||
loadedAdmin = true
|
|
||||||
m.route.set(m.route.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
const onError = function(a, b, c) {
|
|
||||||
elements.forEach(function(x) { x.remove() })
|
|
||||||
loadedAdmin = loadingAdmin = false
|
|
||||||
loaded = 0
|
|
||||||
m.route.set('/logout')
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadAdmin = function(user) {
|
|
||||||
if (loadingAdmin) {
|
|
||||||
if (loadedAdmin) {
|
|
||||||
Authentication.setAdmin(user && user.rank >= 10)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!user || user.rank < 10) return
|
|
||||||
|
|
||||||
loadingAdmin = true
|
|
||||||
|
|
||||||
let token = Authentication.getToken()
|
|
||||||
let element = document.createElement('link')
|
|
||||||
elements.push(element)
|
|
||||||
element.setAttribute('rel', 'stylesheet')
|
|
||||||
element.setAttribute('type', 'text/css')
|
|
||||||
element.setAttribute('href', '/assets/admin.css?token=' + token)
|
|
||||||
element.onload = onLoaded
|
|
||||||
element.onerror = onError
|
|
||||||
document.getElementsByTagName('head')[0].appendChild(element)
|
|
||||||
|
|
||||||
element = document.createElement('script')
|
|
||||||
elements.push(element)
|
|
||||||
element.setAttribute('type', 'text/javascript')
|
|
||||||
element.setAttribute('src', '/assets/admin.js?token=' + token)
|
|
||||||
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)
|
|
||||||
if (Authentication.currentUser) {
|
|
||||||
loadAdmin(Authentication.currentUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Menu = require('./menu/menu')
|
|
||||||
const Footer = require('./footer/footer')
|
|
||||||
const Frontpage = require('./frontpage/frontpage')
|
|
||||||
const Login = require('./login/login')
|
|
||||||
const Logout = require('./login/logout')
|
|
||||||
const Page = require('./pages/page')
|
|
||||||
const Article = require('./article/article')
|
|
||||||
|
|
||||||
const menuRoot = document.getElementById('nav')
|
|
||||||
const mainRoot = document.getElementById('main')
|
|
||||||
const footerRoot = document.getElementById('footer')
|
|
||||||
|
|
||||||
const Loader = {
|
|
||||||
view: function() { return m('div.loading-spinner') },
|
|
||||||
}
|
|
||||||
const AdminResolver = {
|
|
||||||
onmatch: function(args, requestedPath) {
|
|
||||||
if (window.adminRoutes[args.path]) {
|
|
||||||
return window.adminRoutes[args.path][args.id && 1 || 0]
|
|
||||||
}
|
|
||||||
return Loader
|
|
||||||
},
|
|
||||||
render: function(vnode) { return vnode },
|
|
||||||
}
|
|
||||||
|
|
||||||
const allRoutes = {
|
const allRoutes = {
|
||||||
'/': Frontpage,
|
'/': SitePage, // Frontpage
|
||||||
'/login': Login,
|
'/login': Login,
|
||||||
'/logout': Logout,
|
'/page/:id': SitePage,
|
||||||
'/page/:id': Page,
|
'/article/:id': SiteArticle,
|
||||||
'/article/:id': Article,
|
|
||||||
'/admin/:path': AdminResolver,
|
'/admin/:path': AdminResolver,
|
||||||
'/admin/:path/:id': AdminResolver,
|
'/admin/:path/:id': AdminResolver,
|
||||||
}
|
}
|
||||||
|
@ -128,8 +41,8 @@ AVIF.onload = AVIF.onerror = function () {
|
||||||
window.supportsavif = (AVIF.height === 2)
|
window.supportsavif = (AVIF.height === 2)
|
||||||
document.body.className = document.body.className + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly')
|
document.body.className = document.body.className + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly')
|
||||||
|
|
||||||
m.route(mainRoot, '/', allRoutes)
|
m.mount(document.getElementById('header'), Header)
|
||||||
m.mount(menuRoot, Menu)
|
m.route(document.getElementById('main'), '/', allRoutes)
|
||||||
m.mount(footerRoot, Footer)
|
m.mount(document.getElementById('footer'), Footer)
|
||||||
}
|
}
|
||||||
AVIF.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=';
|
AVIF.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=';
|
||||||
|
|
50
nfp_moe/app/media.js
Normal file
50
nfp_moe/app/media.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
export function generateSource(item, cover) {
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
if (item.media_alt_prefix) {
|
||||||
|
item.pictureFallback = item.media_alt_prefix + '_small.jpg'
|
||||||
|
item.pictureJpeg = item.media_alt_prefix + '_small.jpg' + ' 720w, '
|
||||||
|
+ item.media_alt_prefix + '_medium.jpg' + ' 1300w, '
|
||||||
|
+ item.media_alt_prefix + '_large.jpg 1920w'
|
||||||
|
item.pictureAvif = item.media_alt_prefix + '_small.avif' + ' 720w, '
|
||||||
|
+ item.media_alt_prefix + '_medium.avif' + ' 1300w, '
|
||||||
|
+ item.media_alt_prefix + '_large.avif 1920w'
|
||||||
|
|
||||||
|
item.pictureCover = cover
|
||||||
|
} else {
|
||||||
|
item.pictureFallback = null
|
||||||
|
item.pictureJpeg = null
|
||||||
|
item.pictureAvif = null
|
||||||
|
item.pictureCover = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBannerImage(item, prefix) {
|
||||||
|
if (!item || !item.banner_alt_prefix) return null
|
||||||
|
|
||||||
|
let out = {
|
||||||
|
path: prefix + item.path,
|
||||||
|
name: item.name,
|
||||||
|
original: item.banner_path,
|
||||||
|
banner: item.banner_alt_prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviceWidth = window.innerWidth
|
||||||
|
var pixelRatio = window.devicePixelRatio || 1
|
||||||
|
if ((deviceWidth < 720 && pixelRatio <= 1)
|
||||||
|
|| (deviceWidth < 360 && pixelRatio <= 2)) {
|
||||||
|
out.banner += '_small'
|
||||||
|
} else if ((deviceWidth < 1300 && pixelRatio <= 1)
|
||||||
|
|| (deviceWidth < 650 && pixelRatio <= 2)) {
|
||||||
|
out.banner += '_medium'
|
||||||
|
} else {
|
||||||
|
out.banner += '_large'
|
||||||
|
}
|
||||||
|
if (window.supportsavif) {
|
||||||
|
out.banner += '.avif'
|
||||||
|
} else {
|
||||||
|
out.banner += '.jpg'
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
47
nfp_moe/app/page_tree.js
Normal file
47
nfp_moe/app/page_tree.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
const api = require('./api')
|
||||||
|
|
||||||
|
const Tree = window.__nfptree && window.__nfptree.tree || []
|
||||||
|
const TreeMap = new Map()
|
||||||
|
|
||||||
|
exports.Tree = Tree
|
||||||
|
exports.TreeMap = TreeMap
|
||||||
|
|
||||||
|
function parseLeaf(tree) {
|
||||||
|
for (let branch of tree) {
|
||||||
|
TreeMap.set(branch.path, branch)
|
||||||
|
|
||||||
|
if (branch.children && branch.children.length) {
|
||||||
|
parseLeaf(branch.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseLeaf(Tree)
|
||||||
|
|
||||||
|
function processPageBranch(arr, branches, prefix) {
|
||||||
|
branches.forEach((page) => {
|
||||||
|
arr.push({ id: page.id, name: prefix + page.name })
|
||||||
|
if (page.children && page.children.length) {
|
||||||
|
processPageBranch(arr, page.children, page.name + ' -> ')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getFlatTree = function() {
|
||||||
|
let arr = []
|
||||||
|
processPageBranch(arr, Tree, '')
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.refreshTree = function() {
|
||||||
|
return api.sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/pagetree',
|
||||||
|
})
|
||||||
|
.then(pages => {
|
||||||
|
Tree.splice(0, Tree.length)
|
||||||
|
Tree.push.apply(Tree, pages.tree)
|
||||||
|
TreeMap.clear()
|
||||||
|
parseLeaf(Tree)
|
||||||
|
})
|
||||||
|
}
|
39
nfp_moe/app/paginator.js
Normal file
39
nfp_moe/app/paginator.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const Paginator = {
|
||||||
|
view: function(vnode) {
|
||||||
|
let total = vnode.attrs.total
|
||||||
|
let currentPage = vnode.attrs.page
|
||||||
|
let perPage = vnode.attrs.perPage || 10
|
||||||
|
let maxPage = total / perPage + 1
|
||||||
|
|
||||||
|
if (total <= perPage) return null
|
||||||
|
|
||||||
|
return m('paginator', [
|
||||||
|
currentPage > 1
|
||||||
|
? [
|
||||||
|
m(m.route.Link, {
|
||||||
|
href: vnode.attrs.base,
|
||||||
|
}, 'First'),
|
||||||
|
m(m.route.Link, {
|
||||||
|
href: vnode.attrs.base + (currentPage > 2
|
||||||
|
? '?page=' + (currentPage - 1)
|
||||||
|
: ''
|
||||||
|
),
|
||||||
|
}, 'Previous'),
|
||||||
|
]
|
||||||
|
: m('div'),
|
||||||
|
m('div', 'Page ' + currentPage),
|
||||||
|
currentPage < maxPage
|
||||||
|
? [
|
||||||
|
m(m.route.Link, {
|
||||||
|
href: vnode.attrs.base + '?page=' + (currentPage + 1),
|
||||||
|
}, 'Next'),
|
||||||
|
m(m.route.Link, {
|
||||||
|
href: vnode.attrs.base + '?page=' + maxPage,
|
||||||
|
}, 'Last')
|
||||||
|
]
|
||||||
|
: m('div'),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Paginator
|
155
nfp_moe/app/site_article.js
Normal file
155
nfp_moe/app/site_article.js
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const Article = require('./article')
|
||||||
|
const api = require('./api')
|
||||||
|
const media = require('./media')
|
||||||
|
|
||||||
|
window.LoadComments = false
|
||||||
|
window.HyvorLoaded = false
|
||||||
|
window.HYVOR_TALK_WEBSITE = 7544
|
||||||
|
|
||||||
|
const SiteArticle = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.error = ''
|
||||||
|
this.loading = false
|
||||||
|
this.showLoading = null
|
||||||
|
this.data = {
|
||||||
|
article: null,
|
||||||
|
files: [],
|
||||||
|
}
|
||||||
|
this.showcomments = false
|
||||||
|
|
||||||
|
if (window.__nfpdata) {
|
||||||
|
this.path = m.route.param('id')
|
||||||
|
this.data.article = window.__nfpdata
|
||||||
|
window.__nfpdata = null
|
||||||
|
} else {
|
||||||
|
this.fetchArticle(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
api.sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/articles/' + this.path,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.data = result
|
||||||
|
|
||||||
|
if (this.data.article.media_alt_prefix) {
|
||||||
|
this.data.article.pictureFallback = this.data.article.media_alt_prefix + '_small.jpg'
|
||||||
|
this.data.article.pictureJpeg = this.data.article.media_alt_prefix + '_small.jpg' + ' 720w, '
|
||||||
|
+ this.data.article.media_alt_prefix + '_medium.jpg' + ' 1300w, '
|
||||||
|
+ this.data.article.media_alt_prefix + '_large.jpg 1920w'
|
||||||
|
this.data.article.pictureAvif = this.data.article.media_alt_prefix + '_small.avif' + ' 720w, '
|
||||||
|
+ this.data.article.media_alt_prefix + '_medium.avif' + ' 1300w, '
|
||||||
|
+ this.data.article.media_alt_prefix + '_large.avif 1920w'
|
||||||
|
|
||||||
|
this.data.article.pictureCover = '(max-width: 840px) calc(100vw - 82px), '
|
||||||
|
+ '758px'
|
||||||
|
} else {
|
||||||
|
this.data.article.pictureFallback = null
|
||||||
|
this.data.article.pictureJpeg = null
|
||||||
|
this.data.article.pictureAvif = null
|
||||||
|
this.data.article.pictureCover = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.data.article) {
|
||||||
|
this.error = 'Article not found'
|
||||||
|
}
|
||||||
|
}, (err) => {
|
||||||
|
this.error = err.message
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
clearTimeout(this.showLoading)
|
||||||
|
this.showLoading = null
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
let article = this.data.article
|
||||||
|
let banner = media.getBannerImage(article, '/article/')
|
||||||
|
|
||||||
|
return [
|
||||||
|
this.loading
|
||||||
|
? m('div.loading-spinner')
|
||||||
|
: null,
|
||||||
|
!this.loading && this.error
|
||||||
|
? m('div.wrapper', m('div.error', {
|
||||||
|
onclick: () => {
|
||||||
|
this.error = ''
|
||||||
|
this.fetchPage(vnode)
|
||||||
|
},
|
||||||
|
}, 'Page error: ' + this.error + '. Click here to try again'))
|
||||||
|
: null,
|
||||||
|
/*(banner
|
||||||
|
? m('a.page-banner', {
|
||||||
|
href: banner.original,
|
||||||
|
target: '_blank',
|
||||||
|
style: { 'background-image': 'url("' + banner.banner + '")' },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: 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')]
|
||||||
|
),
|
||||||
|
article ? m(Article, { full: true, files: this.data.files, article: article }) : null,
|
||||||
|
window.LoadComments
|
||||||
|
? m('div#hyvor-talk-view', { oncreate: function() {
|
||||||
|
window.HYVOR_TALK_CONFIG = {
|
||||||
|
url: false,
|
||||||
|
id: article.path,
|
||||||
|
loadMode: scroll,
|
||||||
|
}
|
||||||
|
if (!window.HyvorLoaded) {
|
||||||
|
window.HyvorLoaded = true
|
||||||
|
var s = document.createElement('script')
|
||||||
|
s.src = 'https://talk.hyvor.com/web-api/embed.js';
|
||||||
|
s.type = 'text/javascript'
|
||||||
|
// s.setAttribute('crossorigin', '')
|
||||||
|
// s.setAttribute('data-timestamp', +new Date());
|
||||||
|
document.body.appendChild(s);
|
||||||
|
} else {
|
||||||
|
hyvor_talk.reload()
|
||||||
|
}
|
||||||
|
}}, m('div.loading-spinner'))
|
||||||
|
: m('button', {
|
||||||
|
onclick: function() { window.LoadComments = true },
|
||||||
|
}, 'Open comment discussion'),
|
||||||
|
])
|
||||||
|
: null),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SiteArticle
|
90
nfp_moe/app/site_login.js
Normal file
90
nfp_moe/app/site_login.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
const api = require('./api')
|
||||||
|
|
||||||
|
const Login = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.redirect = vnode.attrs.redirect || ''
|
||||||
|
if (Authentication.currentUser) return m.route.set('/')
|
||||||
|
|
||||||
|
this.error = ''
|
||||||
|
this.loading = false
|
||||||
|
this.username = ''
|
||||||
|
this.password = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate: function() {
|
||||||
|
if (Authentication.currentUser) return
|
||||||
|
},
|
||||||
|
|
||||||
|
loginuser: function(vnode, e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!this.username) {
|
||||||
|
this.error = 'Email is missing'
|
||||||
|
} else if (!this.password) {
|
||||||
|
this.error = 'Password is missing'
|
||||||
|
} else {
|
||||||
|
this.error = ''
|
||||||
|
}
|
||||||
|
if (this.error) return
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
api.sendRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/authentication/login',
|
||||||
|
body: {
|
||||||
|
email: this.username,
|
||||||
|
password: this.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (!result.token) {
|
||||||
|
return Promise.reject(new Error('Server authentication down.'))
|
||||||
|
}
|
||||||
|
Authentication.updateToken(result.token)
|
||||||
|
m.route.set(this.redirect || '/')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.error = 'Error while logging into NFP! ' + error.message
|
||||||
|
vnode.state.password = ''
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return [
|
||||||
|
m('div.wrapper', [
|
||||||
|
this.loading ? m('div.loading-spinner') : null,
|
||||||
|
m('form.inside.login', {
|
||||||
|
hidden: this.loading,
|
||||||
|
onsubmit: this.loginuser.bind(this, vnode),
|
||||||
|
}, [
|
||||||
|
m('div.title', 'NFP.moe login'),
|
||||||
|
this.error ? m('div.error', this.error) : null,
|
||||||
|
m('label', 'Email'),
|
||||||
|
m('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: this.username,
|
||||||
|
oninput: (e) => { this.username = e.currentTarget.value },
|
||||||
|
}),
|
||||||
|
m('label', 'Password'),
|
||||||
|
m('input', {
|
||||||
|
type: 'password',
|
||||||
|
value: this.password,
|
||||||
|
oninput: (e) => { this.password = e.currentTarget.value },
|
||||||
|
}),
|
||||||
|
m('input', {
|
||||||
|
type: 'submit',
|
||||||
|
value: 'Log in',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Login
|
224
nfp_moe/app/site_page.js
Normal file
224
nfp_moe/app/site_page.js
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const api = require('./api')
|
||||||
|
const PageTree = require('./page_tree')
|
||||||
|
const Paginator = require('./paginator')
|
||||||
|
// const Authentication = require('./authentication')
|
||||||
|
const Article = require('./article')
|
||||||
|
const Articleslim = require('./article_slim')
|
||||||
|
const media = require('./media')
|
||||||
|
|
||||||
|
const ArticlesPerPage = 10
|
||||||
|
|
||||||
|
const SitePage = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.error = ''
|
||||||
|
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
|
||||||
|
|
||||||
|
if (window.__nfpdata) {
|
||||||
|
this.path = m.route.param('id')
|
||||||
|
this.data = window.__nfpdata
|
||||||
|
|
||||||
|
window.__nfpdata = null
|
||||||
|
window.__nfpsubdata = null
|
||||||
|
} else {
|
||||||
|
this.fetchPage(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onbeforeupdate: function(vnode) {
|
||||||
|
this.currentPage = Number(m.route.param('page')) || 1
|
||||||
|
|
||||||
|
if (this.path !== m.route.param('id') || this.currentPage !== this.lastpage) {
|
||||||
|
this.fetchPage(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchPage: function(vnode) {
|
||||||
|
this.error = ''
|
||||||
|
this.lastpage = this.currentPage
|
||||||
|
this.path = m.route.param('id')
|
||||||
|
|
||||||
|
if (this.showLoading) {
|
||||||
|
clearTimeout(this.showLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.data.page) {
|
||||||
|
this.showLoading = setTimeout(() => {
|
||||||
|
this.showLoading = null
|
||||||
|
this.loading = true
|
||||||
|
m.redraw()
|
||||||
|
}, 300)
|
||||||
|
} else {
|
||||||
|
this.loading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.path) {
|
||||||
|
this.children = PageTree.TreeMap.get(this.path)
|
||||||
|
this.children = this.children && this.children.children || []
|
||||||
|
} else {
|
||||||
|
this.children = PageTree.Tree
|
||||||
|
}
|
||||||
|
|
||||||
|
api.sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/' + (this.path ? 'pages/' + this.path : 'frontpage') + '?page=' + (this.lastpage || 1),
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.data = result
|
||||||
|
|
||||||
|
if (!this.data.page && this.path) {
|
||||||
|
this.error = 'Page not found'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = 'Page not found - NFP Moe - Anime/Manga translation group'
|
||||||
|
if (this.data.page) {
|
||||||
|
title = this.data.page.name + ' - NFP Moe'
|
||||||
|
} else if (!this.path) {
|
||||||
|
title = 'NFP Moe - Anime/Manga translation group'
|
||||||
|
}
|
||||||
|
|
||||||
|
media.generateSource(this.data.page,
|
||||||
|
'(max-width: 840px) calc(100vw - 82px), '
|
||||||
|
+ '758px')
|
||||||
|
|
||||||
|
if (this.lastpage !== 1) {
|
||||||
|
document.title = 'Page ' + this.lastpage + ' - ' + title
|
||||||
|
} else {
|
||||||
|
document.title = title
|
||||||
|
}
|
||||||
|
}, (err) => {
|
||||||
|
this.error = err.message
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
clearTimeout(this.showLoading)
|
||||||
|
this.showLoading = null
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
let page = this.data.page
|
||||||
|
let featuredBanner = media.getBannerImage(this.data.featured, '/article/')
|
||||||
|
let pageBanner = media.getBannerImage(this.data.page, '/page/')
|
||||||
|
let paginatorMaxPath = Math.floor(this.data.total_articles / ArticlesPerPage) + 1
|
||||||
|
let paginatorPrefix = page ? '/page/' + page.path : '/'
|
||||||
|
|
||||||
|
return ([
|
||||||
|
this.loading
|
||||||
|
? m('div.loading-spinner')
|
||||||
|
: null,
|
||||||
|
!this.loading && this.error
|
||||||
|
? m('div.wrapper', m('div.error', {
|
||||||
|
onclick: () => {
|
||||||
|
this.error = ''
|
||||||
|
this.fetchPage(vnode)
|
||||||
|
},
|
||||||
|
}, 'Page error: ' + this.error + '. Click here to try again'))
|
||||||
|
: null,
|
||||||
|
(featuredBanner
|
||||||
|
? m(m.route.Link, {
|
||||||
|
class: 'page-banner',
|
||||||
|
href: featuredBanner.path,
|
||||||
|
style: { 'background-image': 'url("' + featuredBanner.banner + '")' },
|
||||||
|
},
|
||||||
|
m('div.inside', m('div.page-banner-title', featuredBanner.name))
|
||||||
|
)
|
||||||
|
: null),
|
||||||
|
(!featuredBanner && pageBanner
|
||||||
|
? m('a.page-banner', {
|
||||||
|
href: pageBanner.original,
|
||||||
|
target: '_blank',
|
||||||
|
style: { 'background-image': 'url("' + pageBanner.banner + '")' },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null),
|
||||||
|
(page
|
||||||
|
? m('.inside.vertical', [
|
||||||
|
m('div.page-goback', ['« ', m(m.route.Link, {
|
||||||
|
href: page.parent_path
|
||||||
|
? '/page/' + page.parent_path
|
||||||
|
: '/'
|
||||||
|
}, page.parent_name || 'Home')]
|
||||||
|
),
|
||||||
|
m('h2', page.name)
|
||||||
|
])
|
||||||
|
: null),
|
||||||
|
m('.inside', [
|
||||||
|
this.children.length
|
||||||
|
? m('aside', [
|
||||||
|
m('h5', page ? 'View ' + page.name + ':' : 'Categories'),
|
||||||
|
this.children.map((page) => {
|
||||||
|
return [
|
||||||
|
m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name),
|
||||||
|
(page.children && page.children.length
|
||||||
|
? m('ul', page.children.map(function(subpage) {
|
||||||
|
return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name))
|
||||||
|
}))
|
||||||
|
: null),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
m('div.container', [
|
||||||
|
(page && page.pictureFallback
|
||||||
|
? m('a.cover', {
|
||||||
|
rel: 'noopener',
|
||||||
|
href: page.media_path,
|
||||||
|
}, [
|
||||||
|
m('picture', [
|
||||||
|
m('source', {
|
||||||
|
srcset: page.pictureAvif,
|
||||||
|
sizes: page.pictureCover,
|
||||||
|
type: 'image/avif',
|
||||||
|
}),
|
||||||
|
m('img', {
|
||||||
|
srcset: page.pictureJpeg,
|
||||||
|
sizes: page.pictureCover,
|
||||||
|
alt: 'Image for news item ' + page.name,
|
||||||
|
src: page.pictureFallback,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
: null),
|
||||||
|
(page && page.content
|
||||||
|
? page.content.blocks.map(block => {
|
||||||
|
return m(EditorBlock, { block: block })
|
||||||
|
})
|
||||||
|
: null),
|
||||||
|
(page && this.data.articles.length
|
||||||
|
? [
|
||||||
|
m('h5', 'Latest posts under ' + page.name + ':'),
|
||||||
|
this.data.articles.map(function(article) {
|
||||||
|
return m(Articleslim, { article: article })
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: null),
|
||||||
|
(!page && this.data.articles.length
|
||||||
|
? this.data.articles.map(function(article) {
|
||||||
|
return m(Article, { article: article })
|
||||||
|
})
|
||||||
|
: null),
|
||||||
|
m(Paginator, {
|
||||||
|
base: page ? '/page/' + page.path : '/',
|
||||||
|
page: this.currentPage,
|
||||||
|
perPage: ArticlesPerPage,
|
||||||
|
total: this.data.total_articles,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SitePage
|
1
nfp_moe/base
Symbolic link
1
nfp_moe/base
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../base
|
|
@ -1,32 +1,11 @@
|
||||||
import config from './api/config.mjs'
|
import config from '../base/config.mjs'
|
||||||
|
|
||||||
export function start(http, port, ctx) {
|
export function start(http, port, ctx) {
|
||||||
config.stores.overrides.store = ctx.config
|
config.stores.overrides.store = ctx.config
|
||||||
|
|
||||||
return import('./api/server.mjs')
|
return import('./api/server.mjs')
|
||||||
.then(function(server) {
|
.then(function(module) {
|
||||||
return server.run(http, port, ctx)
|
let server = new module.default(http, port, ctx)
|
||||||
|
return server.run()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
import log from './api/log.mjs'
|
|
||||||
|
|
||||||
// Run the database script automatically.
|
|
||||||
import setup from './api/setup.mjs'
|
|
||||||
|
|
||||||
setup().catch(async (error) => {
|
|
||||||
log.error({ code: error.code, message: error.message }, 'Error while preparing database')
|
|
||||||
log.error('Unable to verify database integrity.')
|
|
||||||
log.warn('Continuing anyways')
|
|
||||||
// import('./api/config').then(module => {
|
|
||||||
// log.error(error, 'Error while preparing database')
|
|
||||||
// log.error({ config: module.default.get() }, 'config used')
|
|
||||||
// process.exit(1)
|
|
||||||
// })
|
|
||||||
}).then(() =>
|
|
||||||
import('./api/server.mjs')
|
|
||||||
).catch(error => {
|
|
||||||
log.error(error, 'Unknown error starting server')
|
|
||||||
})
|
|
||||||
*/
|
|
|
@ -9,8 +9,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --experimental-modules index.mjs",
|
"start": "node --experimental-modules index.mjs",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build:prod": "sass -s compressed app/app.scss public/assets/app.css && sass -s compressed app/admin.scss public/assets/admin.css && asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js",
|
"build:prod": "asbundle app/index.js public/assets/app.js && asbundle app/admin/admin.js public/assets/admin.js",
|
||||||
"build": "sass app/app.scss public/assets/app.css && sass app/admin.scss public/assets/admin.css && asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js",
|
"build": "asbundle app/index.js public/assets/app.js && asbundle app/admin/admin.js public/assets/admin.js",
|
||||||
"dev:build": "npm-watch build",
|
"dev:build": "npm-watch build",
|
||||||
"dev:server": "node dev.mjs | bunyan",
|
"dev:server": "node dev.mjs | bunyan",
|
||||||
"dev": "npm-watch dev:server",
|
"dev": "npm-watch dev:server",
|
||||||
|
@ -22,7 +22,9 @@
|
||||||
"watch": {
|
"watch": {
|
||||||
"dev:server": {
|
"dev:server": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
"api/*"
|
"api/*",
|
||||||
|
"base/*",
|
||||||
|
"../base/*"
|
||||||
],
|
],
|
||||||
"extensions": "js,mjs",
|
"extensions": "js,mjs",
|
||||||
"quiet": true,
|
"quiet": true,
|
||||||
|
|
|
@ -23,16 +23,13 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="daymode">
|
<body class="daymode">
|
||||||
<script type="text/javascript" nonce="{{=nonce}}">
|
<script type="text/javascript" nonce="{{=nonce}}">
|
||||||
if (localStorage.getItem('darkmode')) {document.body.className = 'darkmodeon';}
|
if (localStorage.getItem('nfp_sites_darkmode')) {document.body.className = 'darkmode';}
|
||||||
window.__nfptree = {{=payloadTree}};
|
window.__nfptree = {{=payloadTree}};
|
||||||
window.__nfpdata = {{=payloadData}};
|
window.__nfpdata = {{=payloadData}};
|
||||||
window.__nfplinks = {{=payloadLinks}};
|
|
||||||
</script>
|
</script>
|
||||||
<div class="maincontainer">
|
<div id="header"></div>
|
||||||
<div id="nav"></div>
|
|
||||||
<main id="main"></main>
|
<main id="main"></main>
|
||||||
<footer id="footer"></footer>
|
<footer id="footer"></footer>
|
||||||
</div>
|
|
||||||
<script type="text/javascript" src="/assets/app.js?v={{=version}}"></script>
|
<script type="text/javascript" src="/assets/app.js?v={{=version}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
1
nfp_moe_old/.npmrc
Normal file
1
nfp_moe_old/.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package-lock=false
|
18
nfp_moe_old/api/server.mjs
Normal file
18
nfp_moe_old/api/server.mjs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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'
|
||||||
|
|
||||||
|
export default class Server extends Parent {
|
||||||
|
addCustomRoutes() {
|
||||||
|
let page = this.getRouteInstance(PageRoutes)
|
||||||
|
|
||||||
|
let localUtil = new this.core.sc.Util(import.meta.url)
|
||||||
|
this.routes.push(new ServeHandler({
|
||||||
|
pageRoutes: page,
|
||||||
|
root: localUtil.getPathFromRoot('../public'),
|
||||||
|
version: this.core.app.running,
|
||||||
|
frontend: config.get('frontend:url'),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
950
nfp_moe_old/app/admin/dtsel.js
Normal file
950
nfp_moe_old/app/admin/dtsel.js
Normal file
|
@ -0,0 +1,950 @@
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var BODYTYPES = ["DAYS", "MONTHS", "YEARS"];
|
||||||
|
|
||||||
|
/** @typedef {Object.<string, Function[]>} Handlers */
|
||||||
|
/** @typedef {function(String, Function): null} AddHandler */
|
||||||
|
/** @typedef {("DAYS"|"MONTHS"|"YEARS")} BodyType */
|
||||||
|
/** @typedef {string|number} StringNum */
|
||||||
|
/** @typedef {Object.<string, StringNum>} StringNumObj */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The local state
|
||||||
|
* @typedef {Object} InstanceState
|
||||||
|
* @property {Date} value
|
||||||
|
* @property {Number} year
|
||||||
|
* @property {Number} month
|
||||||
|
* @property {Number} day
|
||||||
|
* @property {Number} time
|
||||||
|
* @property {Number} hours
|
||||||
|
* @property {Number} minutes
|
||||||
|
* @property {Number} seconds
|
||||||
|
* @property {BodyType} bodyType
|
||||||
|
* @property {Boolean} visible
|
||||||
|
* @property {Number} cancelBlur
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Config
|
||||||
|
* @property {String} dateFormat
|
||||||
|
* @property {String} timeFormat
|
||||||
|
* @property {Boolean} showDate
|
||||||
|
* @property {Boolean} showTime
|
||||||
|
* @property {Boolean} showSeconds
|
||||||
|
* @property {Number} paddingX
|
||||||
|
* @property {Number} paddingY
|
||||||
|
* @property {BodyType} defaultView
|
||||||
|
* @property {"TOP"|"BOTTOM"} direction
|
||||||
|
* @property {Array} months
|
||||||
|
* @property {Array} monthsShort
|
||||||
|
* @property {Array} weekdaysShort
|
||||||
|
* @property {Array} timeDescr
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @param {HTMLElement} elem
|
||||||
|
* @param {Config} config
|
||||||
|
*/
|
||||||
|
function DTS(elem, config) {
|
||||||
|
var config = config || {};
|
||||||
|
|
||||||
|
/** @type {Config} */
|
||||||
|
var defaultConfig = {
|
||||||
|
defaultView: BODYTYPES[0],
|
||||||
|
dateFormat: "yyyy-mm-dd",
|
||||||
|
timeFormat: "HH:MM:SS",
|
||||||
|
showDate: true,
|
||||||
|
showTime: false,
|
||||||
|
showSeconds: true,
|
||||||
|
paddingX: 5,
|
||||||
|
paddingY: 5,
|
||||||
|
direction: 'TOP',
|
||||||
|
months: [
|
||||||
|
"January", "February", "March", "April", "May", "June",
|
||||||
|
"July", "August", "September", "October", "November", "December"
|
||||||
|
],
|
||||||
|
monthsShort: [
|
||||||
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
||||||
|
],
|
||||||
|
weekdaysShort: [
|
||||||
|
"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"
|
||||||
|
],
|
||||||
|
timeDescr: [
|
||||||
|
"HH:", "MM:", "SS:"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!elem) {
|
||||||
|
throw TypeError("input element or selector required for contructor");
|
||||||
|
}
|
||||||
|
if (Object.getPrototypeOf(elem) === String.prototype) {
|
||||||
|
var _elem = document.querySelectorAll(elem);
|
||||||
|
if (!_elem[0]){
|
||||||
|
throw Error('"' + elem + '" not found.');
|
||||||
|
}
|
||||||
|
elem = _elem[0];
|
||||||
|
}
|
||||||
|
this.config = setDefaults(config, defaultConfig);
|
||||||
|
this.dateFormat = this.config.dateFormat;
|
||||||
|
this.timeFormat = this.config.timeFormat;
|
||||||
|
this.dateFormatRegEx = new RegExp("yyyy|yy|mm|dd", "gi");
|
||||||
|
this.timeFormatRegEx = new RegExp("hh|mm|ss|a", "gi");
|
||||||
|
this.inputElem = elem;
|
||||||
|
this.dtbox = null;
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
DTS.prototype.setup = function () {
|
||||||
|
var handler = this.inputElemHandler.bind(this);
|
||||||
|
this.inputElem.addEventListener("focus", handler, false)
|
||||||
|
this.inputElem.addEventListener("blur", handler, false);
|
||||||
|
}
|
||||||
|
DTS.prototype.inputElemHandler = function (e) {
|
||||||
|
if (e.type == "focus") {
|
||||||
|
if (!this.dtbox) {
|
||||||
|
this.dtbox = new DTBox(e.target, this);
|
||||||
|
}
|
||||||
|
this.dtbox.visible = true;
|
||||||
|
} else if (e.type == "blur" && this.dtbox && this.dtbox.visible) {
|
||||||
|
var self = this;
|
||||||
|
setTimeout(function () {
|
||||||
|
if (self.dtbox.cancelBlur > 0) {
|
||||||
|
self.dtbox.cancelBlur -= 1;
|
||||||
|
} else {
|
||||||
|
self.dtbox.visible = false;
|
||||||
|
self.inputElem.blur();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @param {HTMLElement} elem
|
||||||
|
* @param {DTS} settings
|
||||||
|
*/
|
||||||
|
function DTBox(elem, settings) {
|
||||||
|
/** @type {DTBox} */
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
/** @type {Handlers} */
|
||||||
|
var handlers = {};
|
||||||
|
|
||||||
|
/** @type {InstanceState} */
|
||||||
|
var localState = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} key
|
||||||
|
* @param {*} default_val
|
||||||
|
*/
|
||||||
|
function getterSetter(key, default_val) {
|
||||||
|
return {
|
||||||
|
get: function () {
|
||||||
|
var val = localState[key];
|
||||||
|
return val === undefined ? default_val : val;
|
||||||
|
},
|
||||||
|
set: function (val) {
|
||||||
|
var prevState = self.state;
|
||||||
|
var _handlers = handlers[key] || [];
|
||||||
|
localState[key] = val;
|
||||||
|
for (var i = 0; i < _handlers.length; i++) {
|
||||||
|
_handlers[i].bind(self)(localState, prevState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {AddHandler} */
|
||||||
|
function addHandler(key, handlerFn) {
|
||||||
|
if (!key || !handlerFn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!handlers[key]) {
|
||||||
|
handlers[key] = [];
|
||||||
|
}
|
||||||
|
handlers[key].push(handlerFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperties(this, {
|
||||||
|
visible: getterSetter("visible", false),
|
||||||
|
bodyType: getterSetter("bodyType", settings.config.defaultView),
|
||||||
|
value: getterSetter("value"),
|
||||||
|
year: getterSetter("year", 0),
|
||||||
|
month: getterSetter("month", 0),
|
||||||
|
day: getterSetter("day", 0),
|
||||||
|
hours: getterSetter("hours", 0),
|
||||||
|
minutes: getterSetter("minutes", 0),
|
||||||
|
seconds: getterSetter("seconds", 0),
|
||||||
|
cancelBlur: getterSetter("cancelBlur", 0),
|
||||||
|
addHandler: {value: addHandler},
|
||||||
|
month_long: {
|
||||||
|
get: function () {
|
||||||
|
return self.settings.config.months[self.month];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
month_short: {
|
||||||
|
get: function () {
|
||||||
|
return self.settings.config.monthsShort[self.month]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
get: function () {
|
||||||
|
return Object.assign({}, localState);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
get: function() {
|
||||||
|
var hours = self.hours * 60 * 60 * 1000;
|
||||||
|
var minutes = self.minutes * 60 * 1000;
|
||||||
|
var seconds = self.seconds * 1000;
|
||||||
|
return hours + minutes + seconds;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.el = {};
|
||||||
|
this.settings = settings;
|
||||||
|
this.elem = elem;
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
DTBox.prototype.setup = function () {
|
||||||
|
Object.defineProperties(this.el, {
|
||||||
|
wrapper: { value: null, configurable: true },
|
||||||
|
header: { value: null, configurable: true },
|
||||||
|
body: { value: null, configurable: true },
|
||||||
|
footer: { value: null, configurable: true }
|
||||||
|
});
|
||||||
|
this.setupWrapper();
|
||||||
|
if (this.settings.config.showDate) {
|
||||||
|
this.setupHeader();
|
||||||
|
this.setupBody();
|
||||||
|
}
|
||||||
|
if (this.settings.config.showTime) {
|
||||||
|
this.setupFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
this.addHandler("visible", function (state, prevState) {
|
||||||
|
if (state.visible && !prevState.visible){
|
||||||
|
document.body.appendChild(this.el.wrapper);
|
||||||
|
|
||||||
|
var parts = self.elem.value.split(/\s*,\s*/);
|
||||||
|
var startDate = undefined;
|
||||||
|
var startTime = 0;
|
||||||
|
if (self.settings.config.showDate) {
|
||||||
|
startDate = parseDate(parts[0], self.settings);
|
||||||
|
}
|
||||||
|
if (self.settings.config.showTime) {
|
||||||
|
startTime = parseTime(parts[parts.length-1], self.settings);
|
||||||
|
startTime = startTime || 0;
|
||||||
|
}
|
||||||
|
if (!(startDate && startDate.getTime())) {
|
||||||
|
startDate = new Date();
|
||||||
|
startDate = new Date(
|
||||||
|
startDate.getFullYear(),
|
||||||
|
startDate.getMonth(),
|
||||||
|
startDate.getDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var value = new Date(startDate.getTime() + startTime);
|
||||||
|
self.value = value;
|
||||||
|
self.year = value.getFullYear();
|
||||||
|
self.month = value.getMonth();
|
||||||
|
self.day = value.getDate();
|
||||||
|
self.hours = value.getHours();
|
||||||
|
self.minutes = value.getMinutes();
|
||||||
|
self.seconds = value.getSeconds();
|
||||||
|
|
||||||
|
if (self.settings.config.showDate) {
|
||||||
|
self.setHeaderContent();
|
||||||
|
self.setBodyContent();
|
||||||
|
}
|
||||||
|
if (self.settings.config.showTime) {
|
||||||
|
self.setFooterContent();
|
||||||
|
}
|
||||||
|
} else if (!state.visible && prevState.visible) {
|
||||||
|
document.body.removeChild(this.el.wrapper);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DTBox.prototype.setupWrapper = function () {
|
||||||
|
if (!this.el.wrapper) {
|
||||||
|
var el = document.createElement("div");
|
||||||
|
el.classList.add("date-selector-wrapper");
|
||||||
|
Object.defineProperty(this.el, "wrapper", { value: el });
|
||||||
|
}
|
||||||
|
var self = this;
|
||||||
|
var htmlRoot = document.getElementsByTagName('html')[0];
|
||||||
|
function setPosition(e){
|
||||||
|
var minTopSpace = 300;
|
||||||
|
var box = getOffset(self.elem);
|
||||||
|
var config = self.settings.config;
|
||||||
|
var paddingY = config.paddingY || 5;
|
||||||
|
var paddingX = config.paddingX || 5;
|
||||||
|
var top = box.top + self.elem.offsetHeight + paddingY;
|
||||||
|
var left = box.left + paddingX;
|
||||||
|
var bottom = htmlRoot.clientHeight - box.top + paddingY;
|
||||||
|
|
||||||
|
self.el.wrapper.style.left = `${left}px`;
|
||||||
|
if (box.top > minTopSpace && config.direction != 'BOTTOM') {
|
||||||
|
self.el.wrapper.style.bottom = `${bottom}px`;
|
||||||
|
self.el.wrapper.style.top = '';
|
||||||
|
} else {
|
||||||
|
self.el.wrapper.style.top = `${top}px`;
|
||||||
|
self.el.wrapper.style.bottom = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handler(e) {
|
||||||
|
self.cancelBlur += 1;
|
||||||
|
setTimeout(function(){
|
||||||
|
self.elem.focus();
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
setPosition();
|
||||||
|
this.setPosition = setPosition;
|
||||||
|
this.el.wrapper.addEventListener("mousedown", handler, false);
|
||||||
|
this.el.wrapper.addEventListener("touchstart", handler, false);
|
||||||
|
window.addEventListener('resize', this.setPosition);
|
||||||
|
}
|
||||||
|
DTBox.prototype.setupHeader = function () {
|
||||||
|
if (!this.el.header) {
|
||||||
|
var row = document.createElement("div");
|
||||||
|
var classes = ["cal-nav-prev", "cal-nav-current", "cal-nav-next"];
|
||||||
|
row.classList.add("cal-header");
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
var cell = document.createElement("div");
|
||||||
|
cell.classList.add("cal-nav", classes[i]);
|
||||||
|
cell.onclick = this.onHeaderChange.bind(this);
|
||||||
|
row.appendChild(cell);
|
||||||
|
}
|
||||||
|
row.children[0].innerHTML = "<";
|
||||||
|
row.children[2].innerHTML = ">";
|
||||||
|
Object.defineProperty(this.el, "header", { value: row });
|
||||||
|
tryAppendChild(row, this.el.wrapper);
|
||||||
|
}
|
||||||
|
this.setHeaderContent();
|
||||||
|
}
|
||||||
|
DTBox.prototype.setHeaderContent = function () {
|
||||||
|
var content = this.year;
|
||||||
|
if ("DAYS" == this.bodyType) {
|
||||||
|
content = this.month_long + " " + content;
|
||||||
|
} else if ("YEARS" == this.bodyType) {
|
||||||
|
var start = this.year + 10 - (this.year % 10);
|
||||||
|
content = start - 10 + "-" + (start - 1);
|
||||||
|
}
|
||||||
|
this.el.header.children[1].innerText = content;
|
||||||
|
}
|
||||||
|
DTBox.prototype.setupBody = function () {
|
||||||
|
if (!this.el.body) {
|
||||||
|
var el = document.createElement("div");
|
||||||
|
el.classList.add("cal-body");
|
||||||
|
Object.defineProperty(this.el, "body", { value: el });
|
||||||
|
tryAppendChild(el, this.el.wrapper);
|
||||||
|
}
|
||||||
|
var toAppend = null;
|
||||||
|
function makeGrid(rows, cols, className, firstRowClass, clickHandler) {
|
||||||
|
var grid = document.createElement("div");
|
||||||
|
grid.classList.add(className);
|
||||||
|
for (var i = 1; i < rows + 1; i++) {
|
||||||
|
var row = document.createElement("div");
|
||||||
|
row.classList.add("cal-row", "cal-row-" + i);
|
||||||
|
if (i == 1 && firstRowClass) {
|
||||||
|
row.classList.add(firstRowClass);
|
||||||
|
}
|
||||||
|
for (var j = 1; j < cols + 1; j++) {
|
||||||
|
var col = document.createElement("div");
|
||||||
|
col.classList.add("cal-cell", "cal-col-" + j);
|
||||||
|
col.onclick = clickHandler;
|
||||||
|
row.appendChild(col);
|
||||||
|
}
|
||||||
|
grid.appendChild(row);
|
||||||
|
}
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
if ("DAYS" == this.bodyType) {
|
||||||
|
toAppend = this.el.body.calDays;
|
||||||
|
if (!toAppend) {
|
||||||
|
toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this));
|
||||||
|
for (var i = 0; i < 7; i++) {
|
||||||
|
var cell = toAppend.children[0].children[i];
|
||||||
|
cell.innerText = this.settings.config.weekdaysShort[i];
|
||||||
|
cell.onclick = null;
|
||||||
|
}
|
||||||
|
this.el.body.calDays = toAppend;
|
||||||
|
}
|
||||||
|
} else if ("MONTHS" == this.bodyType) {
|
||||||
|
toAppend = this.el.body.calMonths;
|
||||||
|
if (!toAppend) {
|
||||||
|
toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this));
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
for (var j = 0; j < 4; j++) {
|
||||||
|
var monthShort = this.settings.config.monthsShort[4 * i + j];
|
||||||
|
toAppend.children[i].children[j].innerText = monthShort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.el.body.calMonths = toAppend;
|
||||||
|
}
|
||||||
|
} else if ("YEARS" == this.bodyType) {
|
||||||
|
toAppend = this.el.body.calYears;
|
||||||
|
if (!toAppend) {
|
||||||
|
toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this));
|
||||||
|
this.el.body.calYears = toAppend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
empty(this.el.body);
|
||||||
|
tryAppendChild(toAppend, this.el.body);
|
||||||
|
this.setBodyContent();
|
||||||
|
}
|
||||||
|
DTBox.prototype.setBodyContent = function () {
|
||||||
|
var grid = this.el.body.children[0];
|
||||||
|
var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"];
|
||||||
|
if ("DAYS" == this.bodyType) {
|
||||||
|
var oneDayMilliSecs = 24 * 60 * 60 * 1000;
|
||||||
|
var start = new Date(this.year, this.month, 1);
|
||||||
|
var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay());
|
||||||
|
|
||||||
|
grid.children[6].style.display = "";
|
||||||
|
for (var i = 1; i < 7; i++) {
|
||||||
|
for (var j = 0; j < 7; j++) {
|
||||||
|
var cell = grid.children[i].children[j];
|
||||||
|
var month = adjusted.getMonth();
|
||||||
|
var date = adjusted.getDate();
|
||||||
|
|
||||||
|
cell.innerText = date;
|
||||||
|
cell.classList.remove(classes[0], classes[1], classes[2]);
|
||||||
|
if (month != this.month) {
|
||||||
|
if (i == 6 && j == 0) {
|
||||||
|
grid.children[6].style.display = "none";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cell.classList.add(month < this.month ? classes[0] : classes[1]);
|
||||||
|
} else if (isEqualDate(adjusted, this.value)){
|
||||||
|
cell.classList.add(classes[2]);
|
||||||
|
}
|
||||||
|
adjusted = new Date(adjusted.getTime() + oneDayMilliSecs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("YEARS" == this.bodyType) {
|
||||||
|
var year = this.year - (this.year % 10) - 1;
|
||||||
|
for (i = 0; i < 3; i++) {
|
||||||
|
for (j = 0; j < 4; j++) {
|
||||||
|
grid.children[i].children[j].innerText = year;
|
||||||
|
year += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grid.children[0].children[0].classList.add(classes[0]);
|
||||||
|
grid.children[2].children[3].classList.add(classes[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Event} e */
|
||||||
|
DTBox.prototype.onTimeChange = function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.type == 'mousedown') {
|
||||||
|
this.cancelBlur += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.type == 'mouseup') {
|
||||||
|
var self = this;
|
||||||
|
setTimeout(function(){
|
||||||
|
self.elem.focus();
|
||||||
|
}, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var el = e.target;
|
||||||
|
this[el.name] = parseInt(el.value) || 0;
|
||||||
|
this.setupFooter();
|
||||||
|
this.setInputValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
DTBox.prototype.setupFooter = function() {
|
||||||
|
if (!this.el.footer) {
|
||||||
|
var footer = document.createElement("div");
|
||||||
|
var handler = this.onTimeChange.bind(this);
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
function makeRow(label, name, range, changeHandler) {
|
||||||
|
var row = document.createElement("div");
|
||||||
|
row.classList.add('cal-time');
|
||||||
|
|
||||||
|
var labelCol = row.appendChild(document.createElement("div"));
|
||||||
|
labelCol.classList.add('cal-time-label');
|
||||||
|
labelCol.innerText = label;
|
||||||
|
|
||||||
|
var valueCol = row.appendChild(document.createElement("div"));
|
||||||
|
valueCol.classList.add('cal-time-value');
|
||||||
|
valueCol.innerText = '00';
|
||||||
|
|
||||||
|
var inputCol = row.appendChild(document.createElement("div"));
|
||||||
|
var slider = inputCol.appendChild(document.createElement("input"));
|
||||||
|
Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'});
|
||||||
|
Object.defineProperty(footer, name, {value: slider});
|
||||||
|
inputCol.classList.add('cal-time-slider');
|
||||||
|
slider.onchange = changeHandler;
|
||||||
|
slider.oninput = changeHandler;
|
||||||
|
slider.onmousedown = changeHandler;
|
||||||
|
slider.onmouseup = changeHandler;
|
||||||
|
self[name] = self[name] || parseInt(slider.value) || 0;
|
||||||
|
footer.appendChild(row)
|
||||||
|
}
|
||||||
|
makeRow(this.settings.config.timeDescr[0], 'hours', 23, handler);
|
||||||
|
makeRow(this.settings.config.timeDescr[1], 'minutes', 59, handler);
|
||||||
|
if (this.settings.config.showSeconds) {
|
||||||
|
makeRow(this.settings.config.timeDescr[2], 'seconds', 59, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer.classList.add("cal-footer");
|
||||||
|
Object.defineProperty(this.el, "footer", { value: footer });
|
||||||
|
tryAppendChild(footer, this.el.wrapper);
|
||||||
|
}
|
||||||
|
this.setFooterContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
DTBox.prototype.setFooterContent = function() {
|
||||||
|
if (this.el.footer) {
|
||||||
|
var footer = this.el.footer;
|
||||||
|
footer.hours.value = this.hours;
|
||||||
|
footer.children[0].children[1].innerText = padded(this.hours, 2);
|
||||||
|
footer.minutes.value = this.minutes;
|
||||||
|
footer.children[1].children[1].innerText = padded(this.minutes, 2);
|
||||||
|
if (this.settings.config.showSeconds) {
|
||||||
|
footer.seconds.value = this.seconds;
|
||||||
|
footer.children[2].children[1].innerText = padded(this.seconds, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DTBox.prototype.setInputValue = function() {
|
||||||
|
var date = new Date(this.year, this.month, this.day);
|
||||||
|
var strings = [];
|
||||||
|
if (this.settings.config.showDate) {
|
||||||
|
strings.push(renderDate(date, this.settings));
|
||||||
|
}
|
||||||
|
if (this.settings.config.showTime) {
|
||||||
|
var joined = new Date(date.getTime() + this.time);
|
||||||
|
strings.push(renderTime(joined, this.settings));
|
||||||
|
}
|
||||||
|
this.elem.value = strings.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
DTBox.prototype.onDateSelected = function (e) {
|
||||||
|
var row = e.target.parentNode;
|
||||||
|
var date = parseInt(e.target.innerText);
|
||||||
|
if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) {
|
||||||
|
this.month += 1;
|
||||||
|
} else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) {
|
||||||
|
this.month -= 1;
|
||||||
|
}
|
||||||
|
this.day = parseInt(e.target.innerText);
|
||||||
|
this.value = new Date(this.year, this.month, this.day);
|
||||||
|
this.setInputValue();
|
||||||
|
this.setHeaderContent();
|
||||||
|
this.setBodyContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Event} e */
|
||||||
|
DTBox.prototype.onMonthSelected = function (e) {
|
||||||
|
var col = 0;
|
||||||
|
var row = 2;
|
||||||
|
var cell = e.target;
|
||||||
|
if (cell.parentNode.nextSibling){
|
||||||
|
row = cell.parentNode.previousSibling ? 1: 0;
|
||||||
|
}
|
||||||
|
if (cell.previousSibling) {
|
||||||
|
col = 3;
|
||||||
|
if (cell.nextSibling) {
|
||||||
|
col = cell.previousSibling.previousSibling ? 2 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.month = 4 * row + col;
|
||||||
|
this.bodyType = "DAYS";
|
||||||
|
this.setHeaderContent();
|
||||||
|
this.setupBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Event} e */
|
||||||
|
DTBox.prototype.onYearSelected = function (e) {
|
||||||
|
this.year = parseInt(e.target.innerText);
|
||||||
|
this.bodyType = "MONTHS";
|
||||||
|
this.setHeaderContent();
|
||||||
|
this.setupBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Event} e */
|
||||||
|
DTBox.prototype.onHeaderChange = function (e) {
|
||||||
|
var cell = e.target;
|
||||||
|
if (cell.previousSibling && cell.nextSibling) {
|
||||||
|
var idx = BODYTYPES.indexOf(this.bodyType);
|
||||||
|
if (idx < 0 || !BODYTYPES[idx + 1]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.bodyType = BODYTYPES[idx + 1];
|
||||||
|
this.setupBody();
|
||||||
|
} else {
|
||||||
|
var sign = cell.previousSibling ? 1 : -1;
|
||||||
|
switch (this.bodyType) {
|
||||||
|
case "DAYS":
|
||||||
|
this.month += sign * 1;
|
||||||
|
break;
|
||||||
|
case "MONTHS":
|
||||||
|
this.year += sign * 1;
|
||||||
|
break;
|
||||||
|
case "YEARS":
|
||||||
|
this.year += sign * 10;
|
||||||
|
}
|
||||||
|
if (this.month > 11 || this.month < 0) {
|
||||||
|
this.year += Math.floor(this.month / 11);
|
||||||
|
this.month = this.month > 11 ? 0 : 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setHeaderContent();
|
||||||
|
this.setBodyContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} elem
|
||||||
|
* @returns {{left:number, top:number}}
|
||||||
|
*/
|
||||||
|
function getOffset(elem) {
|
||||||
|
var box = elem.getBoundingClientRect();
|
||||||
|
var left = window.pageXOffset !== undefined ? window.pageXOffset :
|
||||||
|
(document.documentElement || document.body.parentNode || document.body).scrollLeft;
|
||||||
|
var top = window.pageYOffset !== undefined ? window.pageYOffset :
|
||||||
|
(document.documentElement || document.body.parentNode || document.body).scrollTop;
|
||||||
|
return { left: box.left + left, top: box.top + top };
|
||||||
|
}
|
||||||
|
function empty(e) {
|
||||||
|
for (; e.children.length; ) e.removeChild(e.children[0]);
|
||||||
|
}
|
||||||
|
function tryAppendChild(newChild, refNode) {
|
||||||
|
try {
|
||||||
|
refNode.appendChild(newChild);
|
||||||
|
return newChild;
|
||||||
|
} catch (e) {
|
||||||
|
console.trace(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @class */
|
||||||
|
function hookFuncs() {
|
||||||
|
/** @type {Handlers} */
|
||||||
|
this._funcs = {};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
* @param {Function} func
|
||||||
|
*/
|
||||||
|
hookFuncs.prototype.add = function(key, func){
|
||||||
|
if (!this._funcs[key]){
|
||||||
|
this._funcs[key] = [];
|
||||||
|
}
|
||||||
|
this._funcs[key].push(func)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {String} key
|
||||||
|
* @returns {Function[]} handlers
|
||||||
|
*/
|
||||||
|
hookFuncs.prototype.get = function(key){
|
||||||
|
return this._funcs[key] ? this._funcs[key] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array.<string>} arr
|
||||||
|
* @param {String} string
|
||||||
|
* @returns {Array.<string>} sorted string
|
||||||
|
*/
|
||||||
|
function sortByStringIndex(arr, string) {
|
||||||
|
return arr.sort(function(a, b){
|
||||||
|
var h = string.indexOf(a);
|
||||||
|
var l = string.indexOf(b);
|
||||||
|
var rank = 0;
|
||||||
|
if (h < l) {
|
||||||
|
rank = -1;
|
||||||
|
} else if (l < h) {
|
||||||
|
rank = 1;
|
||||||
|
} else if (a.length > b.length) {
|
||||||
|
rank = -1;
|
||||||
|
} else if (b.length > a.length) {
|
||||||
|
rank = 1;
|
||||||
|
}
|
||||||
|
return rank;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove keys from array that are not in format
|
||||||
|
* @param {string[]} keys
|
||||||
|
* @param {string} format
|
||||||
|
* @returns {string[]} new filtered array
|
||||||
|
*/
|
||||||
|
function filterFormatKeys(keys, format) {
|
||||||
|
var out = [];
|
||||||
|
var formatIdx = 0;
|
||||||
|
for (var i = 0; i<keys.length; i++) {
|
||||||
|
var key = keys[i];
|
||||||
|
if (format.slice(formatIdx).indexOf(key) > -1) {
|
||||||
|
formatIdx += key.length;
|
||||||
|
out.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {StringNumObj} FormatObj
|
||||||
|
* @param {string} value
|
||||||
|
* @param {string} format
|
||||||
|
* @param {FormatObj} formatObj
|
||||||
|
* @param {function(Object.<string, hookFuncs>): null} setHooks
|
||||||
|
* @returns {FormatObj} formatObj
|
||||||
|
*/
|
||||||
|
function parseData(value, format, formatObj, setHooks) {
|
||||||
|
var hooks = {
|
||||||
|
canSkip: new hookFuncs(),
|
||||||
|
updateValue: new hookFuncs(),
|
||||||
|
}
|
||||||
|
var keys = sortByStringIndex(Object.keys(formatObj), format);
|
||||||
|
var filterdKeys = filterFormatKeys(keys, format);
|
||||||
|
var vstart = 0; // value start
|
||||||
|
if (setHooks) {
|
||||||
|
setHooks(hooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
var key = keys[i];
|
||||||
|
var fstart = format.indexOf(key);
|
||||||
|
var _vstart = vstart; // next value start
|
||||||
|
var val = null;
|
||||||
|
var canSkip = false;
|
||||||
|
var funcs = hooks.canSkip.get(key);
|
||||||
|
|
||||||
|
vstart = vstart || fstart;
|
||||||
|
|
||||||
|
for (var j = 0; j < funcs.length; j++) {
|
||||||
|
if (funcs[j](formatObj)){
|
||||||
|
canSkip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fstart > -1 && !canSkip) {
|
||||||
|
var sep = null;
|
||||||
|
var stop = vstart + key.length;
|
||||||
|
var fnext = -1;
|
||||||
|
var nextKeyIdx = i + 1;
|
||||||
|
_vstart += key.length; // set next value start if current key is found
|
||||||
|
|
||||||
|
// get next format token used to determine separator
|
||||||
|
while (fnext == -1 && nextKeyIdx < keys.length){
|
||||||
|
var nextKey = keys[nextKeyIdx];
|
||||||
|
nextKeyIdx += 1;
|
||||||
|
if (filterdKeys.indexOf(nextKey) === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start
|
||||||
|
}
|
||||||
|
if (fnext > -1){
|
||||||
|
sep = format.slice(stop, fnext);
|
||||||
|
if (sep) {
|
||||||
|
var _stop = value.slice(vstart).indexOf(sep);
|
||||||
|
if (_stop && _stop > -1){
|
||||||
|
stop = _stop + vstart;
|
||||||
|
_vstart = stop + sep.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val = parseInt(value.slice(vstart, stop));
|
||||||
|
|
||||||
|
var funcs = hooks.updateValue.get(key);
|
||||||
|
for (var k = 0; k < funcs.length; k++) {
|
||||||
|
val = funcs[k](val, formatObj, vstart, stop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formatObj[key] = { index: vstart, value: val };
|
||||||
|
vstart = _vstart; // set next value start
|
||||||
|
}
|
||||||
|
return formatObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} value
|
||||||
|
* @param {DTS} settings
|
||||||
|
* @returns {Date} date object
|
||||||
|
*/
|
||||||
|
function parseDate(value, settings) {
|
||||||
|
/** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */
|
||||||
|
var formatObj = {yyyy:null, yy:null, mm:null, dd:null};
|
||||||
|
var format = ((settings.dateFormat) || '').toLowerCase();
|
||||||
|
if (!format) {
|
||||||
|
throw new TypeError('dateFormat not found (' + settings.dateFormat + ')');
|
||||||
|
}
|
||||||
|
var formatObj = parseData(value, format, formatObj, function(hooks){
|
||||||
|
hooks.canSkip.add("yy", function(data){
|
||||||
|
return data["yyyy"].value;
|
||||||
|
});
|
||||||
|
hooks.updateValue.add("yy", function(val){
|
||||||
|
return 100 * Math.floor(new Date().getFullYear() / 100) + val;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var year = formatObj["yyyy"].value || formatObj["yy"].value;
|
||||||
|
var month = formatObj["mm"].value - 1;
|
||||||
|
var date = formatObj["dd"].value;
|
||||||
|
var result = new Date(year, month, date);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} value
|
||||||
|
* @param {DTS} settings
|
||||||
|
* @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1
|
||||||
|
*/
|
||||||
|
function parseTime(value, settings) {
|
||||||
|
var format = ((settings.timeFormat) || '').toLowerCase();
|
||||||
|
if (!format) {
|
||||||
|
throw new TypeError('timeFormat not found (' + settings.timeFormat + ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */
|
||||||
|
var formatObj = {hh:null, mm:null, ss:null, a:null};
|
||||||
|
var formatObj = parseData(value, format, formatObj, function(hooks){
|
||||||
|
hooks.updateValue.add("a", function(val, data, start, stop){
|
||||||
|
return value.slice(start, start + 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var hours = formatObj["hh"].value;
|
||||||
|
var minutes = formatObj["mm"].value;
|
||||||
|
var seconds = formatObj["ss"].value;
|
||||||
|
var am_pm = formatObj["a"].value;
|
||||||
|
var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm;
|
||||||
|
if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){
|
||||||
|
if (am_pm_lower == 'am' && hours == 12){
|
||||||
|
hours = 0;
|
||||||
|
} else if (am_pm_lower == 'pm') {
|
||||||
|
hours += 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Date} value
|
||||||
|
* @param {DTS} settings
|
||||||
|
* @returns {String} date string
|
||||||
|
*/
|
||||||
|
function renderDate(value, settings) {
|
||||||
|
var format = settings.dateFormat.toLowerCase();
|
||||||
|
var date = value.getDate();
|
||||||
|
var month = value.getMonth() + 1;
|
||||||
|
var year = value.getFullYear();
|
||||||
|
var yearShort = year % 100;
|
||||||
|
var formatObj = {
|
||||||
|
dd: date < 10 ? "0" + date : date,
|
||||||
|
mm: month < 10 ? "0" + month : month,
|
||||||
|
yyyy: year,
|
||||||
|
yy: yearShort < 10 ? "0" + yearShort : yearShort
|
||||||
|
};
|
||||||
|
var str = format.replace(settings.dateFormatRegEx, function (found) {
|
||||||
|
return formatObj[found];
|
||||||
|
});
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Date} value
|
||||||
|
* @param {DTS} settings
|
||||||
|
* @returns {String} date string
|
||||||
|
*/
|
||||||
|
function renderTime(value, settings) {
|
||||||
|
var Format = settings.timeFormat;
|
||||||
|
var format = Format.toLowerCase();
|
||||||
|
var hours = value.getHours();
|
||||||
|
var minutes = value.getMinutes();
|
||||||
|
var seconds = value.getSeconds();
|
||||||
|
var am_pm = null;
|
||||||
|
var hh_am_pm = null;
|
||||||
|
if (format.indexOf('a') > -1) {
|
||||||
|
am_pm = hours >= 12 ? 'pm' : 'am';
|
||||||
|
am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm;
|
||||||
|
hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours);
|
||||||
|
}
|
||||||
|
var formatObj = {
|
||||||
|
hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours),
|
||||||
|
mm: minutes < 10 ? "0" + minutes : minutes,
|
||||||
|
ss: seconds < 10 ? "0" + seconds : seconds,
|
||||||
|
a: am_pm,
|
||||||
|
};
|
||||||
|
var str = format.replace(settings.timeFormatRegEx, function (found) {
|
||||||
|
return formatObj[found];
|
||||||
|
});
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if two dates are equal
|
||||||
|
* @param {Date} date1
|
||||||
|
* @param {Date} date2
|
||||||
|
* @returns {Boolean} true or false
|
||||||
|
*/
|
||||||
|
function isEqualDate(date1, date2) {
|
||||||
|
if (!(date1 && date2)) return false;
|
||||||
|
return (date1.getFullYear() == date2.getFullYear() &&
|
||||||
|
date1.getMonth() == date2.getMonth() &&
|
||||||
|
date1.getDate() == date2.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Number} val
|
||||||
|
* @param {Number} pad
|
||||||
|
* @param {*} default_val
|
||||||
|
* @returns {String} padded string
|
||||||
|
*/
|
||||||
|
function padded(val, pad, default_val) {
|
||||||
|
var default_val = default_val || 0;
|
||||||
|
var valStr = '' + (parseInt(val) || default_val);
|
||||||
|
var diff = Math.max(pad, valStr.length) - valStr.length;
|
||||||
|
return ('' + default_val).repeat(diff) + valStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template X
|
||||||
|
* @template Y
|
||||||
|
* @param {X} obj
|
||||||
|
* @param {Y} objDefaults
|
||||||
|
* @returns {X|Y} merged object
|
||||||
|
*/
|
||||||
|
function setDefaults(obj, objDefaults) {
|
||||||
|
var keys = Object.keys(objDefaults);
|
||||||
|
for (var i=0; i<keys.length; i++) {
|
||||||
|
var key = keys[i];
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
obj[key] = objDefaults[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.dtsel = Object.create({},{
|
||||||
|
DTS: { value: DTS },
|
||||||
|
DTObj: { value: DTBox },
|
||||||
|
fn: {
|
||||||
|
value: Object.defineProperties({}, {
|
||||||
|
empty: { value: empty },
|
||||||
|
appendAfter: {
|
||||||
|
value: function (newElem, refNode) {
|
||||||
|
refNode.parentNode.insertBefore(newElem, refNode.nextSibling);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getOffset: { value: getOffset },
|
||||||
|
parseDate: { value: parseDate },
|
||||||
|
renderDate: { value: renderDate },
|
||||||
|
parseTime: {value: parseTime},
|
||||||
|
renderTime: {value: renderTime},
|
||||||
|
setDefaults: {value: setDefaults},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})();
|
348
nfp_moe_old/app/admin/editarticle.js
Normal file
348
nfp_moe_old/app/admin/editarticle.js
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
require('./dtsel')
|
||||||
|
const FileUpload = require('../widgets/fileupload')
|
||||||
|
const Page = require('../api/page')
|
||||||
|
const Fileinfo = require('../widgets/fileinfo')
|
||||||
|
const common = require('../api/common')
|
||||||
|
const Editor = require('./editor')
|
||||||
|
|
||||||
|
const EditArticle = {
|
||||||
|
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.loading = false
|
||||||
|
this.showLoading = null
|
||||||
|
this.data = {
|
||||||
|
article: null,
|
||||||
|
files: [],
|
||||||
|
staff: [],
|
||||||
|
}
|
||||||
|
this.pages = [{id: null, name: 'Frontpage'}]
|
||||||
|
this.pages = this.pages.concat(Page.getFlatTree())
|
||||||
|
this.newBanner = null
|
||||||
|
this.newMedia = null
|
||||||
|
this.dateInstance = null
|
||||||
|
this.editor = null
|
||||||
|
|
||||||
|
this.fetchArticle(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
onbeforeupdate: function(vnode) {
|
||||||
|
if (this.lastid !== m.route.param('id')) {
|
||||||
|
this.fetchArticle(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchArticle: function(vnode) {
|
||||||
|
this.lastid = m.route.param('id')
|
||||||
|
|
||||||
|
return this.requestArticle(
|
||||||
|
common.sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
|
requestArticle: function(data) {
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
data
|
||||||
|
.then((result) => {
|
||||||
|
this.data = result
|
||||||
|
this.data.article.publish_at = new Date(this.data.article.publish_at)
|
||||||
|
|
||||||
|
if (this.data.article.id) {
|
||||||
|
document.title = 'Editing: ' + this.data.article.name + ' - Admin NFP Moe'
|
||||||
|
this.editedPath = true
|
||||||
|
} else {
|
||||||
|
document.title = 'Create Article - Admin NFP Moe'
|
||||||
|
}
|
||||||
|
}, (err) => {
|
||||||
|
this.error = err.message
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
clearTimeout(this.showLoading)
|
||||||
|
this.showLoading = null
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValue: function(name, e) {
|
||||||
|
if (name === 'is_featured') {
|
||||||
|
this.data.article[name] = e.currentTarget.checked
|
||||||
|
} else {
|
||||||
|
this.data.article[name] = e.currentTarget.value
|
||||||
|
}
|
||||||
|
if (name === 'path') {
|
||||||
|
this.editedPath = true
|
||||||
|
} else if (name === 'name' && !this.editedPath) {
|
||||||
|
this.data.article.path = this.data.article.name.toLowerCase().replace(/ /g, '-')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateParent: function(e) {
|
||||||
|
this.data.article.page_id = Number(e.currentTarget.value) || null
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStaffer: function(e) {
|
||||||
|
this.data.article.admin_id = Number(e.currentTarget.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
mediaUploaded: function(type, file) {
|
||||||
|
if (type === 'banner') {
|
||||||
|
this.newBanner = file
|
||||||
|
} else {
|
||||||
|
this.newMedia = file
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mediaRemoved: function(type) {
|
||||||
|
this.data.article['remove_' + type] = true
|
||||||
|
this.data.article[type + '_prefix'] = null
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function(vnode, e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!this.data.article.name) {
|
||||||
|
this.error = 'Name is missing'
|
||||||
|
} else if (!this.data.article.path) {
|
||||||
|
this.error = 'Path is missing'
|
||||||
|
} else {
|
||||||
|
this.error = ''
|
||||||
|
}
|
||||||
|
if (this.error) return
|
||||||
|
|
||||||
|
let formData = new FormData()
|
||||||
|
if (this.newBanner) {
|
||||||
|
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 || this.data.staff[0].id)
|
||||||
|
formData.append('name', this.data.article.name)
|
||||||
|
formData.append('is_featured', this.data.article.is_featured || false)
|
||||||
|
formData.append('path', this.data.article.path)
|
||||||
|
formData.append('page_id', this.data.article.page_id || null)
|
||||||
|
formData.append('publish_at', this.dateInstance.inputElem.value.replace(', ', 'T') + 'Z')
|
||||||
|
formData.append('remove_banner', this.data.article.remove_banner ? true : false)
|
||||||
|
formData.append('remove_media', this.data.article.remove_media ? true : false)
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.requestArticle(
|
||||||
|
this.editor.save()
|
||||||
|
.then(body => {
|
||||||
|
formData.append('content', JSON.stringify(body))
|
||||||
|
|
||||||
|
return common.sendRequest({
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (!data.article.id) {
|
||||||
|
throw new Error('Something went wrong with saving, try again later')
|
||||||
|
} else if (this.lastid === 'add') {
|
||||||
|
this.lastid = data.article.id.toString()
|
||||||
|
m.route.set('/admin/articles/' + data.article.id)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadFile: function(vnode, e) {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
const showPublish = this.data.article
|
||||||
|
? this.data.article.publish_at > new Date()
|
||||||
|
: false
|
||||||
|
const bannerImage = this.data.article && this.data.article.banner_prefix
|
||||||
|
? this.data.article.banner_prefix + '_large.avif'
|
||||||
|
: null
|
||||||
|
const mediaImage = this.data.article && this.data.article.media_prefix
|
||||||
|
? this.data.article.media_prefix + '_large.avif'
|
||||||
|
: null
|
||||||
|
|
||||||
|
return [
|
||||||
|
this.loading && !this.data.article
|
||||||
|
? m('div.admin-spinner.loading-spinner')
|
||||||
|
: null,
|
||||||
|
this.data.article
|
||||||
|
? m('div.admin-wrapper', [
|
||||||
|
this.loading
|
||||||
|
? m('div.loading-spinner')
|
||||||
|
: null,
|
||||||
|
m('div.admin-actions', this.data.article.id
|
||||||
|
? [
|
||||||
|
m('span', 'Actions:'),
|
||||||
|
m(m.route.Link, { href: '/article/' + this.data.article.path }, 'View article'),
|
||||||
|
]
|
||||||
|
: null),
|
||||||
|
m('article.editarticle', [
|
||||||
|
m('header', m('h1',
|
||||||
|
(this.data.article.id ? 'Edit ' : 'Create Article ') + (this.data.article.name || '(untitled)')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
m('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (this.data.article.name || '(untitled)'))),
|
||||||
|
m('div.error', {
|
||||||
|
hidden: !this.error,
|
||||||
|
onclick: () => { vnode.state.error = '' },
|
||||||
|
}, this.error),
|
||||||
|
m(FileUpload, {
|
||||||
|
height: 300,
|
||||||
|
onfile: this.mediaUploaded.bind(this, 'banner'),
|
||||||
|
ondelete: this.mediaRemoved.bind(this, 'banner'),
|
||||||
|
media: bannerImage,
|
||||||
|
}),
|
||||||
|
m(FileUpload, {
|
||||||
|
class: 'cover',
|
||||||
|
useimg: true,
|
||||||
|
onfile: this.mediaUploaded.bind(this, 'media'),
|
||||||
|
ondelete: this.mediaRemoved.bind(this, 'media'),
|
||||||
|
media: mediaImage,
|
||||||
|
}),
|
||||||
|
m('form.editarticle.content', {
|
||||||
|
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 === this.data.article.page_id
|
||||||
|
}, item.name)
|
||||||
|
})),
|
||||||
|
m('div.input-row', [
|
||||||
|
m('div.input-group', [
|
||||||
|
m('label', 'Name'),
|
||||||
|
m('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: this.data.article.name,
|
||||||
|
oninput: this.updateValue.bind(this, 'name'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
m('div.input-group', [
|
||||||
|
m('label', 'Path'),
|
||||||
|
m('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: this.data.article.path,
|
||||||
|
oninput: this.updateValue.bind(this, 'path'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m('label', 'Description'),
|
||||||
|
m(Editor, {
|
||||||
|
oncreate: (subnode) => {
|
||||||
|
this.editor = subnode.state.editor
|
||||||
|
},
|
||||||
|
contentdata: this.data.article.content,
|
||||||
|
}),
|
||||||
|
m('div.input-row', [
|
||||||
|
m('div.input-group', [
|
||||||
|
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: this.data.article.publish_at.toISOString().replace('T', ', ').split('.')[0],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
m('div.input-group', [
|
||||||
|
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 === this.data.article.admin_id
|
||||||
|
}, item.name)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
m('div.input-group.small', [
|
||||||
|
m('label', 'Make featured'),
|
||||||
|
m('input', {
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: this.data.article.is_featured,
|
||||||
|
oninput: this.updateValue.bind(this, 'is_featured'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m('div', {
|
||||||
|
hidden: !this.data.article.name || !this.data.article.path
|
||||||
|
}, [
|
||||||
|
m('input', {
|
||||||
|
type: 'submit',
|
||||||
|
value: 'Save',
|
||||||
|
}),
|
||||||
|
showPublish
|
||||||
|
? m('button.submit', {
|
||||||
|
onclick: () => {
|
||||||
|
this.data.article.publish_at = new Date().toISOString()
|
||||||
|
}
|
||||||
|
}, 'Publish')
|
||||||
|
: null,
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
this.data.files.length
|
||||||
|
? m('files', [
|
||||||
|
m('h4', 'Files'),
|
||||||
|
this.data.files.map((file) => {
|
||||||
|
return m(Fileinfo, { file: file })
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
this.data.article.id
|
||||||
|
? m('div.fileupload', [
|
||||||
|
'Add file',
|
||||||
|
m('input', {
|
||||||
|
accept: '*',
|
||||||
|
type: 'file',
|
||||||
|
onchange: this.uploadFile.bind(this, vnode),
|
||||||
|
}),
|
||||||
|
(vnode.state.loadingFile ? m('div.loading-spinner') : null),
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
: m('div.error', {
|
||||||
|
hidden: !this.error,
|
||||||
|
onclick: () => { this.fetchArticle(vnode) },
|
||||||
|
}, this.error),,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EditArticle
|
47
nfp_moe_old/app/admin/editor.js
Normal file
47
nfp_moe_old/app/admin/editor.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
const Editor = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.editor = null
|
||||||
|
this.lastData = null
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate: function(vnode) {
|
||||||
|
this.editor = new window.EditorJS({
|
||||||
|
holder: vnode.dom,
|
||||||
|
inlineToolbar: ['link', 'bold', 'inlineCode', 'italic'],
|
||||||
|
tools: {
|
||||||
|
inlineCode: {
|
||||||
|
class: window.InlineCode, //<span class="inline-code"></span>
|
||||||
|
shortcut: 'CMD+SHIFT+M',
|
||||||
|
},
|
||||||
|
header: window.Header,
|
||||||
|
image: window.SimpleImage,
|
||||||
|
quote: window.Quote,
|
||||||
|
code: window.CodeTool,
|
||||||
|
list: {
|
||||||
|
class: window.List,
|
||||||
|
inlineToolbar: true,
|
||||||
|
config: {
|
||||||
|
defaultStyle: 'unordered'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delimiter: window.Delimiter,
|
||||||
|
htmlraw: window.RawTool,
|
||||||
|
},
|
||||||
|
data: vnode.attrs.contentdata,
|
||||||
|
})
|
||||||
|
this.lastData = vnode.attrs.contentdata
|
||||||
|
},
|
||||||
|
|
||||||
|
onupdate: function(vnode) {
|
||||||
|
if (this.lastData !== vnode.attrs.contentdata) {
|
||||||
|
this.lastData = vnode.attrs.contentdata
|
||||||
|
this.editor.render(this.lastData)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return m('div')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Editor
|
262
nfp_moe_old/app/admin/editpage.js
Normal file
262
nfp_moe_old/app/admin/editpage.js
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
const FileUpload = require('../widgets/fileupload')
|
||||||
|
const Page = require('../api/page.p')
|
||||||
|
|
||||||
|
const common = require('../api/common')
|
||||||
|
const Editor = require('./editor')
|
||||||
|
|
||||||
|
const EditPage = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.loading = false
|
||||||
|
this.showLoading = null
|
||||||
|
this.data = {
|
||||||
|
page: null,
|
||||||
|
}
|
||||||
|
this.pages = [{id: null, name: 'Frontpage'}]
|
||||||
|
this.pages = this.pages.concat(Page.getFlatTree())
|
||||||
|
|
||||||
|
this.newBanner = null
|
||||||
|
this.newMedia = null
|
||||||
|
this.editor = null
|
||||||
|
|
||||||
|
this.fetchPage(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
onbeforeupdate: function(vnode) {
|
||||||
|
if (this.lastid !== m.route.param('id')) {
|
||||||
|
this.fetchPage(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchPage: function(vnode) {
|
||||||
|
this.lastid = m.route.param('id')
|
||||||
|
|
||||||
|
return this.requestPage(
|
||||||
|
common.sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
|
requestPage: function(data) {
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
if (this.showLoading) {
|
||||||
|
clearTimeout(this.showLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.data.page) {
|
||||||
|
this.showLoading = setTimeout(() => {
|
||||||
|
this.showLoading = null
|
||||||
|
this.loading = true
|
||||||
|
m.redraw()
|
||||||
|
}, 150)
|
||||||
|
} else {
|
||||||
|
this.loading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
data
|
||||||
|
.then((result) => {
|
||||||
|
this.data = result
|
||||||
|
if (this.data.page.id) {
|
||||||
|
document.title = 'Editing: ' + this.data.page.name + ' - Admin NFP Moe'
|
||||||
|
this.editedPath = true
|
||||||
|
} else {
|
||||||
|
document.title = 'Create Page - Admin NFP Moe'
|
||||||
|
}
|
||||||
|
}, (err) => {
|
||||||
|
this.error = err.message
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
clearTimeout(this.showLoading)
|
||||||
|
this.showLoading = null
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValue: function(name, e) {
|
||||||
|
this.data.page[name] = e.currentTarget.value
|
||||||
|
if (name === 'path') {
|
||||||
|
this.editedPath = true
|
||||||
|
} else if (name === 'name' && !this.editedPath) {
|
||||||
|
this.data.page.path = this.data.page.name.toLowerCase().replace(/ /g, '-')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateParent: function(e) {
|
||||||
|
this.data.page.parent_id = Number(e.currentTarget.value) || null
|
||||||
|
},
|
||||||
|
|
||||||
|
mediaUploaded: function(type, file) {
|
||||||
|
if (type === 'banner') {
|
||||||
|
this.newBanner = file
|
||||||
|
} else {
|
||||||
|
this.newMedia = file
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mediaRemoved: function(type) {
|
||||||
|
this.data.page['remove_' + type] = true
|
||||||
|
this.data.page[type + '_prefix'] = null
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function(vnode, e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!this.data.page.name) {
|
||||||
|
this.error = 'Name is missing'
|
||||||
|
} else if (!this.data.page.path) {
|
||||||
|
this.error = 'Path is missing'
|
||||||
|
} else {
|
||||||
|
this.error = ''
|
||||||
|
}
|
||||||
|
if (this.error) return
|
||||||
|
|
||||||
|
let formData = new FormData()
|
||||||
|
if (this.newBanner) {
|
||||||
|
formData.append('banner', this.newBanner.file)
|
||||||
|
}
|
||||||
|
if (this.newMedia) {
|
||||||
|
formData.append('media', this.newMedia.file)
|
||||||
|
}
|
||||||
|
if (this.data.page.id) {
|
||||||
|
formData.append('id', this.data.page.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.append('name', this.data.page.name)
|
||||||
|
formData.append('parent_id', this.data.page.parent_id || null)
|
||||||
|
formData.append('path', this.data.page.path)
|
||||||
|
formData.append('remove_banner', this.data.page.remove_banner ? true : false)
|
||||||
|
formData.append('remove_media', this.data.page.remove_media ? true : false)
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.requestPage(
|
||||||
|
this.editor.save()
|
||||||
|
.then(body => {
|
||||||
|
formData.append('content', JSON.stringify(body))
|
||||||
|
|
||||||
|
return common.sendRequest({
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (!data.page.id) {
|
||||||
|
throw new Error('Something went wrong with saving, try again later')
|
||||||
|
} else if (this.lastid === 'add') {
|
||||||
|
this.lastid = data.page.id.toString()
|
||||||
|
m.route.set('/admin/pages/' + data.page.id)
|
||||||
|
}
|
||||||
|
return Page.refreshTree().then(() => {
|
||||||
|
this.pages = [{id: null, name: 'Frontpage'}]
|
||||||
|
this.pages = this.pages.concat(Page.getFlatTree())
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
const bannerImage = this.data.page && this.data.page.banner_prefix
|
||||||
|
? this.data.page.banner_prefix + '_large.avif'
|
||||||
|
: null
|
||||||
|
const mediaImage = this.data.page && this.data.page.media_prefix
|
||||||
|
? this.data.page.media_prefix + '_large.avif'
|
||||||
|
: null
|
||||||
|
|
||||||
|
return [
|
||||||
|
this.loading && !this.data.page
|
||||||
|
? m('div.admin-spinner.loading-spinner')
|
||||||
|
: null,
|
||||||
|
this.data.page
|
||||||
|
? m('div.admin-wrapper', [
|
||||||
|
this.loading
|
||||||
|
? m('div.loading-spinner')
|
||||||
|
: null,
|
||||||
|
m('div.admin-actions', this.data.page.id
|
||||||
|
? [
|
||||||
|
m('span', 'Actions:'),
|
||||||
|
m(m.route.Link, { href: '/page/' + this.data.page.path }, 'View page'),
|
||||||
|
]
|
||||||
|
: null),
|
||||||
|
m('article.editarticle', [
|
||||||
|
m('header', m('h1',
|
||||||
|
(this.data.page.id ? 'Edit ' : 'Create Page ') + (this.data.page.name || '(untitled)')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
m('div.error', {
|
||||||
|
hidden: !this.error,
|
||||||
|
onclick: () => { vnode.state.error = '' },
|
||||||
|
}, this.error),
|
||||||
|
m(FileUpload, {
|
||||||
|
height: 300,
|
||||||
|
onfile: this.mediaUploaded.bind(this, 'banner'),
|
||||||
|
ondelete: this.mediaRemoved.bind(this, 'banner'),
|
||||||
|
media: bannerImage,
|
||||||
|
}),
|
||||||
|
m(FileUpload, {
|
||||||
|
class: 'cover',
|
||||||
|
useimg: true,
|
||||||
|
onfile: this.mediaUploaded.bind(this, 'media'),
|
||||||
|
ondelete: this.mediaRemoved.bind(this, 'media'),
|
||||||
|
media: mediaImage,
|
||||||
|
}),
|
||||||
|
m('form.editarticle.content', {
|
||||||
|
onsubmit: this.save.bind(this, vnode),
|
||||||
|
}, [
|
||||||
|
m('label', 'Parent'),
|
||||||
|
m('select', {
|
||||||
|
onchange: this.updateParent.bind(this),
|
||||||
|
}, this.pages.filter(item => !this.data.page || item.id !== this.data.page.id).map((item) => {
|
||||||
|
return m('option', {
|
||||||
|
value: item.id || 0,
|
||||||
|
selected: item.id === this.data.page.parent_id
|
||||||
|
}, item.name)
|
||||||
|
})),
|
||||||
|
m('div.input-row', [
|
||||||
|
m('div.input-group', [
|
||||||
|
m('label', 'Name'),
|
||||||
|
m('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: this.data.page.name,
|
||||||
|
oninput: this.updateValue.bind(this, 'name'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
m('div.input-group', [
|
||||||
|
m('label', 'Path'),
|
||||||
|
m('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: this.data.page.path,
|
||||||
|
oninput: this.updateValue.bind(this, 'path'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
m('label', 'Description'),
|
||||||
|
m(Editor, {
|
||||||
|
oncreate: (subnode) => {
|
||||||
|
this.editor = subnode.state.editor
|
||||||
|
},
|
||||||
|
contentdata: this.data.page.content,
|
||||||
|
}),
|
||||||
|
m('div', {
|
||||||
|
hidden: !this.data.page.name || !this.data.page.path
|
||||||
|
}, [
|
||||||
|
m('input', {
|
||||||
|
type: 'submit',
|
||||||
|
value: 'Save',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
: m('div.error', {
|
||||||
|
hidden: !this.error,
|
||||||
|
onclick: () => { this.fetchPage(vnode) },
|
||||||
|
}, this.error),,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EditPage
|
152
nfp_moe_old/app/admin/editstaff.js
Normal file
152
nfp_moe_old/app/admin/editstaff.js
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
const Staff = require('../api/staff')
|
||||||
|
|
||||||
|
const EditStaff = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.fetchStaff(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
onupdate: function(vnode) {
|
||||||
|
if (this.lastid !== m.route.param('id')) {
|
||||||
|
this.fetchStaff(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchStaff: function(vnode) {
|
||||||
|
this.lastid = m.route.param('id')
|
||||||
|
this.loading = this.lastid !== 'add'
|
||||||
|
this.creating = this.lastid === 'add'
|
||||||
|
this.error = ''
|
||||||
|
this.staff = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
rank: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lastid !== 'add') {
|
||||||
|
Staff.getStaff(this.lastid)
|
||||||
|
.then(function(result) {
|
||||||
|
vnode.state.editedPath = true
|
||||||
|
vnode.state.staff = result
|
||||||
|
document.title = 'Editing: ' + result.name + ' - Admin NFP Moe'
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
vnode.state.error = err.message
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
vnode.state.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
document.title = 'Creating Staff Member - Admin NFP Moe'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValue: function(key, e) {
|
||||||
|
this.staff[key] = e.currentTarget.value
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function(vnode, e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!this.staff.name) {
|
||||||
|
this.error = 'Fullname is missing'
|
||||||
|
} else if (!this.staff.email) {
|
||||||
|
this.error = 'Email is missing'
|
||||||
|
} else {
|
||||||
|
this.error = ''
|
||||||
|
}
|
||||||
|
if (this.error) return
|
||||||
|
|
||||||
|
this.staff.description = vnode.state.froala && vnode.state.froala.html.get() || this.staff.description
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
let promise
|
||||||
|
|
||||||
|
if (this.staff.id) {
|
||||||
|
promise = Staff.updateStaff(this.staff.id, {
|
||||||
|
name: this.staff.name,
|
||||||
|
email: this.staff.email,
|
||||||
|
rank: this.staff.rank,
|
||||||
|
password: this.staff.password,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
promise = Staff.createStaff({
|
||||||
|
name: this.staff.name,
|
||||||
|
email: this.staff.email,
|
||||||
|
rank: this.staff.rank,
|
||||||
|
password: this.staff.password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.then(function(res) {
|
||||||
|
m.route.set('/admin/staff')
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
vnode.state.error = err.message
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
vnode.state.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateLevel: function(e) {
|
||||||
|
this.staff.rank = Number(e.currentTarget.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
const ranks = [[10, 'Manager'], [100, 'Admin']]
|
||||||
|
return (
|
||||||
|
this.loading ?
|
||||||
|
m('div.loading-spinner')
|
||||||
|
: m('div.admin-wrapper', [
|
||||||
|
m('div.admin-actions', this.staff.id
|
||||||
|
? [
|
||||||
|
m('span', 'Actions:'),
|
||||||
|
m(m.route.Link, { href: '/admin/staff' }, 'Staff list'),
|
||||||
|
]
|
||||||
|
: null),
|
||||||
|
m('article.editstaff', [
|
||||||
|
m('header', m('h1', this.creating ? 'Create Staff' : 'Edit ' + (this.staff.name || '(untitled)'))),
|
||||||
|
m('div.error', {
|
||||||
|
hidden: !this.error,
|
||||||
|
onclick: function() { vnode.state.error = '' },
|
||||||
|
}, this.error),
|
||||||
|
m('form.editstaff.content', {
|
||||||
|
onsubmit: this.save.bind(this, vnode),
|
||||||
|
}, [
|
||||||
|
m('label', 'Level'),
|
||||||
|
m('select', {
|
||||||
|
onchange: this.updateLevel.bind(this),
|
||||||
|
}, 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.name,
|
||||||
|
oninput: this.updateValue.bind(this, 'name'),
|
||||||
|
}),
|
||||||
|
m('label', 'Email'),
|
||||||
|
m('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: this.staff.email,
|
||||||
|
oninput: this.updateValue.bind(this, 'email'),
|
||||||
|
}),
|
||||||
|
m('label', 'Password (optional)'),
|
||||||
|
m('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: this.staff.password,
|
||||||
|
oninput: this.updateValue.bind(this, 'password'),
|
||||||
|
}),
|
||||||
|
m('input', {
|
||||||
|
type: 'submit',
|
||||||
|
value: 'Save',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EditStaff
|
46
nfp_moe_old/app/admin/froala.js
Normal file
46
nfp_moe_old/app/admin/froala.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
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
|
110
nfp_moe_old/app/admin/stafflist.js
Normal file
110
nfp_moe_old/app/admin/stafflist.js
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
const Staff = require('../api/staff')
|
||||||
|
const Dialogue = require('../widgets/dialogue')
|
||||||
|
const Pages = require('../widgets/pages')
|
||||||
|
|
||||||
|
const AdminStaffList = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.error = ''
|
||||||
|
this.lastpage = m.route.param('page') || '1'
|
||||||
|
this.staff = []
|
||||||
|
this.removeStaff = null
|
||||||
|
|
||||||
|
this.fetchStaffs(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchStaffs: function(vnode) {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
document.title = 'Staff members - Admin NFP Moe'
|
||||||
|
|
||||||
|
return Staff.getAllStaff()
|
||||||
|
.then(function(result) {
|
||||||
|
vnode.state.staff = result
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
vnode.state.error = err.message
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
vnode.state.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmRemoveStaff: function(vnode) {
|
||||||
|
let removingStaff = this.removeStaff
|
||||||
|
this.removeStaff = null
|
||||||
|
this.loading = true
|
||||||
|
Staff.removeStaff(removingStaff.id)
|
||||||
|
.then(this.oninit.bind(this, vnode))
|
||||||
|
.catch(function(err) {
|
||||||
|
vnode.state.error = err.message
|
||||||
|
vnode.state.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getLevel: function(level) {
|
||||||
|
if (level === 100) {
|
||||||
|
return 'Admin'
|
||||||
|
}
|
||||||
|
return 'Manager'
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return [
|
||||||
|
m('div.admin-wrapper', [
|
||||||
|
m('div.admin-actions', [
|
||||||
|
m('span', 'Actions:'),
|
||||||
|
m(m.route.Link, { href: '/admin/staff/add' }, 'Create new staff'),
|
||||||
|
]),
|
||||||
|
m('article.editarticle', [
|
||||||
|
m('header', m('h1', 'All staff')),
|
||||||
|
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', 'Fullname'),
|
||||||
|
m('th', 'Email'),
|
||||||
|
m('th', 'Level'),
|
||||||
|
m('th.right', 'Updated'),
|
||||||
|
m('th.right', 'Actions'),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
m('tbody', this.staff.map(function(item) {
|
||||||
|
return m('tr', [
|
||||||
|
m('td', m(m.route.Link, { href: '/admin/staff/' + item.id }, item.name)),
|
||||||
|
m('td', item.email),
|
||||||
|
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')),
|
||||||
|
])
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
m(Pages, {
|
||||||
|
base: '/admin/staff',
|
||||||
|
links: this.links,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
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.name : '') + '" (' + (vnode.state.removeStaff ? vnode.state.removeStaff.email : '') + ')',
|
||||||
|
yes: 'Remove',
|
||||||
|
yesclass: 'alert',
|
||||||
|
no: 'Cancel',
|
||||||
|
noclass: 'cancel',
|
||||||
|
onyes: this.confirmRemoveStaff.bind(this, vnode),
|
||||||
|
onno: function() { vnode.state.removeStaff = null },
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AdminStaffList
|
|
@ -12,10 +12,6 @@ const Article = {
|
||||||
this.data = {
|
this.data = {
|
||||||
article: null,
|
article: null,
|
||||||
files: [],
|
files: [],
|
||||||
pictureFallback: null,
|
|
||||||
pictureJpeg: null,
|
|
||||||
pictureAvif: null,
|
|
||||||
pictureCover: null,
|
|
||||||
}
|
}
|
||||||
this.showcomments = false
|
this.showcomments = false
|
||||||
|
|
42
nfp_moe_old/app/authentication.js
Normal file
42
nfp_moe_old/app/authentication.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
const storageName = 'nfp_sites_logintoken'
|
||||||
|
|
||||||
|
const Authentication = {
|
||||||
|
currentUser: null,
|
||||||
|
isAdmin: false,
|
||||||
|
loadingListeners: [],
|
||||||
|
authListeners: [],
|
||||||
|
|
||||||
|
updateToken: function(token) {
|
||||||
|
if (!token) return Authentication.clearToken()
|
||||||
|
localStorage.setItem(storageName, token)
|
||||||
|
Authentication.currentUser = JSON.parse(atob(token.split('.')[1]))
|
||||||
|
|
||||||
|
if (Authentication.authListeners.length) {
|
||||||
|
Authentication.authListeners.forEach(function(x) { x(Authentication.currentUser) })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearToken: function() {
|
||||||
|
Authentication.currentUser = null
|
||||||
|
localStorage.removeItem(storageName)
|
||||||
|
Authentication.isAdmin = false
|
||||||
|
},
|
||||||
|
|
||||||
|
addEvent: function(event) {
|
||||||
|
Authentication.authListeners.push(event)
|
||||||
|
},
|
||||||
|
|
||||||
|
setAdmin: function(item) {
|
||||||
|
Authentication.isAdmin = item
|
||||||
|
},
|
||||||
|
|
||||||
|
getToken: function() {
|
||||||
|
return localStorage.getItem(storageName)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Authentication.updateToken(localStorage.getItem(storageName))
|
||||||
|
|
||||||
|
window.Authentication = Authentication
|
||||||
|
|
||||||
|
module.exports = Authentication
|
135
nfp_moe_old/app/index.js
Normal file
135
nfp_moe_old/app/index.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
require('./polyfill')
|
||||||
|
|
||||||
|
const m = require('mithril')
|
||||||
|
window.m = m
|
||||||
|
|
||||||
|
m.route.setOrig = m.route.set
|
||||||
|
m.route.set = function(path, data, options){
|
||||||
|
m.route.setOrig(path, data, options)
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.route.linkOrig = m.route.link
|
||||||
|
m.route.link = function(vnode){
|
||||||
|
m.route.linkOrig(vnode)
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
|
||||||
|
m.route.prefix = ''
|
||||||
|
window.adminRoutes = {}
|
||||||
|
let loadingAdmin = false
|
||||||
|
let loadedAdmin = false
|
||||||
|
let loaded = 0
|
||||||
|
let elements = []
|
||||||
|
|
||||||
|
const onLoaded = function() {
|
||||||
|
loaded++
|
||||||
|
if (loaded < 2) return
|
||||||
|
|
||||||
|
Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.rank >= 10)
|
||||||
|
loadedAdmin = true
|
||||||
|
m.route.set(m.route.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onError = function(a, b, c) {
|
||||||
|
elements.forEach(function(x) { x.remove() })
|
||||||
|
loadedAdmin = loadingAdmin = false
|
||||||
|
loaded = 0
|
||||||
|
m.route.set('/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAdmin = function(user) {
|
||||||
|
if (loadingAdmin) {
|
||||||
|
if (loadedAdmin) {
|
||||||
|
Authentication.setAdmin(user && user.rank >= 10)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!user || user.rank < 10) return
|
||||||
|
|
||||||
|
loadingAdmin = true
|
||||||
|
|
||||||
|
let token = Authentication.getToken()
|
||||||
|
let element = document.createElement('link')
|
||||||
|
elements.push(element)
|
||||||
|
element.setAttribute('rel', 'stylesheet')
|
||||||
|
element.setAttribute('type', 'text/css')
|
||||||
|
element.setAttribute('href', '/assets/admin.css?token=' + token)
|
||||||
|
element.onload = onLoaded
|
||||||
|
element.onerror = onError
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(element)
|
||||||
|
|
||||||
|
element = document.createElement('script')
|
||||||
|
elements.push(element)
|
||||||
|
element.setAttribute('type', 'text/javascript')
|
||||||
|
element.setAttribute('src', '/assets/admin.js?token=' + token)
|
||||||
|
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)
|
||||||
|
if (Authentication.currentUser) {
|
||||||
|
loadAdmin(Authentication.currentUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Menu = require('./menu/menu')
|
||||||
|
const Footer = require('./footer/footer')
|
||||||
|
const Frontpage = require('./frontpage/frontpage')
|
||||||
|
const Login = require('./login/login')
|
||||||
|
const Logout = require('./login/logout')
|
||||||
|
const Page = require('./pages/page')
|
||||||
|
const Article = require('./article/article')
|
||||||
|
|
||||||
|
const menuRoot = document.getElementById('nav')
|
||||||
|
const mainRoot = document.getElementById('main')
|
||||||
|
const footerRoot = document.getElementById('footer')
|
||||||
|
|
||||||
|
const Loader = {
|
||||||
|
view: function() { return m('div.loading-spinner') },
|
||||||
|
}
|
||||||
|
const AdminResolver = {
|
||||||
|
onmatch: function(args, requestedPath) {
|
||||||
|
if (window.adminRoutes[args.path]) {
|
||||||
|
return window.adminRoutes[args.path][args.id && 1 || 0]
|
||||||
|
}
|
||||||
|
return Loader
|
||||||
|
},
|
||||||
|
render: function(vnode) { return vnode },
|
||||||
|
}
|
||||||
|
|
||||||
|
const allRoutes = {
|
||||||
|
'/': Frontpage,
|
||||||
|
'/login': Login,
|
||||||
|
'/logout': Logout,
|
||||||
|
'/page/:id': Page,
|
||||||
|
'/article/:id': Article,
|
||||||
|
'/admin/:path': AdminResolver,
|
||||||
|
'/admin/:path/:id': AdminResolver,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until we finish checking avif support, some views render immediately and will ask for this immediately before the callback gets called.
|
||||||
|
|
||||||
|
/*
|
||||||
|
* imgsupport.js from leechy/imgsupport
|
||||||
|
*/
|
||||||
|
const AVIF = new Image();
|
||||||
|
AVIF.onload = AVIF.onerror = function () {
|
||||||
|
window.supportsavif = (AVIF.height === 2)
|
||||||
|
document.body.className = document.body.className + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly')
|
||||||
|
|
||||||
|
m.route(mainRoot, '/', allRoutes)
|
||||||
|
m.mount(menuRoot, Menu)
|
||||||
|
m.mount(footerRoot, Footer)
|
||||||
|
}
|
||||||
|
AVIF.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=';
|
|
@ -70,6 +70,24 @@ const Page = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.data.page.media_alt_prefix) {
|
||||||
|
this.data.page.pictureFallback = this.data.page.media_alt_prefix + '_small.jpg'
|
||||||
|
this.data.page.pictureJpeg = this.data.page.media_alt_prefix + '_small.jpg' + ' 720w, '
|
||||||
|
+ this.data.page.media_alt_prefix + '_medium.jpg' + ' 1300w, '
|
||||||
|
+ this.data.page.media_alt_prefix + '_large.jpg 1920w'
|
||||||
|
this.data.page.pictureAvif = this.data.page.media_alt_prefix + '_small.avif' + ' 720w, '
|
||||||
|
+ this.data.page.media_alt_prefix + '_medium.avif' + ' 1300w, '
|
||||||
|
+ this.data.page.media_alt_prefix + '_large.avif 1920w'
|
||||||
|
|
||||||
|
this.data.page.pictureCover = '(max-width: 840px) calc(100vw - 82px), '
|
||||||
|
+ '758px'
|
||||||
|
} else {
|
||||||
|
this.data.page.pictureFallback = null
|
||||||
|
this.data.page.pictureJpeg = null
|
||||||
|
this.data.page.pictureAvif = null
|
||||||
|
this.data.page.pictureCover = null
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lastpage !== 1) {
|
if (this.lastpage !== 1) {
|
||||||
document.title = 'Page ' + this.lastpage + ' - ' + this.data.page.name + ' - NFP Moe'
|
document.title = 'Page ' + this.lastpage + ' - ' + this.data.page.name + ' - NFP Moe'
|
||||||
} else {
|
} else {
|
||||||
|
@ -87,30 +105,8 @@ const Page = {
|
||||||
},
|
},
|
||||||
|
|
||||||
view: function(vnode) {
|
view: function(vnode) {
|
||||||
var deviceWidth = window.innerWidth
|
let page = this.data.page
|
||||||
var pixelRatio = window.devicePixelRatio || 1
|
let bannerPath = ''
|
||||||
var bannerPath = ''
|
|
||||||
var imagePath = ''
|
|
||||||
|
|
||||||
if (this.data.page && this.data.page.banner) {
|
|
||||||
if (deviceWidth < 400 && pixelRatio <= 1) {
|
|
||||||
bannerPath = this.data.page.banner.small_url
|
|
||||||
} else if ((deviceWidth < 800 && pixelRatio <= 1)
|
|
||||||
|| (deviceWidth < 600 && pixelRatio > 1)) {
|
|
||||||
bannerPath = this.data.page.banner.medium_url
|
|
||||||
} else {
|
|
||||||
bannerPath = this.data.page.banner.large_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.data.page && this.data.page.media) {
|
|
||||||
if ((deviceWidth < 1000 && pixelRatio <= 1)
|
|
||||||
|| (deviceWidth < 800 && pixelRatio > 1)) {
|
|
||||||
imagePath = this.data.page.media.medium_url
|
|
||||||
} else {
|
|
||||||
imagePath = this.data.page.media.large_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ([
|
return ([
|
||||||
this.loading
|
this.loading
|
||||||
|
@ -130,36 +126,55 @@ const Page = {
|
||||||
? m('.div.page-banner', { style: { 'background-image': 'url("' + bannerPath + '")' } } )
|
? m('.div.page-banner', { style: { 'background-image': 'url("' + bannerPath + '")' } } )
|
||||||
: null,
|
: null,
|
||||||
m('div.goback', ['« ', m(m.route.Link, {
|
m('div.goback', ['« ', m(m.route.Link, {
|
||||||
href: this.data.page.parent_path
|
href: page.parent_path
|
||||||
? '/page/' + this.data.page.parent_path
|
? '/page/' + page.parent_path
|
||||||
: '/'
|
: '/'
|
||||||
}, this.data.page.parent_name || 'Home')]),
|
}, page.parent_name || 'Home')]),
|
||||||
m('header', m('h1', this.data.page.name)),
|
m('header', m('h1', page.name)),
|
||||||
m('.container', {
|
m('.container', {
|
||||||
class: this.children.length ? 'multi' : '',
|
class: this.children.length ? 'multi' : '',
|
||||||
}, [
|
}, [
|
||||||
this.children.length
|
this.children.length
|
||||||
? m('aside.sidebar', [
|
? m('aside.sidebar', [
|
||||||
m('h4', 'View ' + this.data.page.name + ':'),
|
m('h4', 'View ' + page.name + ':'),
|
||||||
this.children.map(function(page) {
|
this.children.map(function(page) {
|
||||||
return m(m.route.Link, { href: '/page/' + page.path }, page.name)
|
return m(m.route.Link, { href: '/page/' + page.path }, page.name)
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
: null,
|
: null,
|
||||||
this.data.page.content
|
page.content
|
||||||
? m('.fr-view', [
|
? m('.fr-view', [
|
||||||
imagePath
|
page.pictureFallback
|
||||||
? m('a', { href: this.data.page.media.link}, m('img.page-cover', { src: imagePath, alt: 'Cover image for ' + this.data.page.name } ))
|
? m('a.cover', {
|
||||||
|
rel: 'noopener',
|
||||||
|
href: page.media_path,
|
||||||
|
}, [
|
||||||
|
m('picture', [
|
||||||
|
m('source', {
|
||||||
|
srcset: page.pictureAvif,
|
||||||
|
sizes: page.pictureCover,
|
||||||
|
type: 'image/avif',
|
||||||
|
}),
|
||||||
|
m('img', {
|
||||||
|
srcset: page.pictureJpeg,
|
||||||
|
sizes: page.pictureCover,
|
||||||
|
alt: 'Image for news item ' + page.name,
|
||||||
|
src: page.pictureFallback,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
])
|
||||||
: null,
|
: null,
|
||||||
m.trust(this.data.page.content),
|
page.content.blocks.map(block => {
|
||||||
this.data.articles.length && this.data.page.content
|
return m(EditorBlock, { block: block })
|
||||||
|
}),
|
||||||
|
this.data.articles.length && page.content
|
||||||
? m('aside.news', [
|
? m('aside.news', [
|
||||||
m('h4', 'Latest posts under ' + this.data.page.name + ':'),
|
m('h4', 'Latest posts under ' + page.name + ':'),
|
||||||
this.data.articles.map(function(article) {
|
this.data.articles.map(function(article) {
|
||||||
return m(Newsentry, { article: article })
|
return m(Newsentry, { article: article })
|
||||||
}),
|
}),
|
||||||
m(Pages, {
|
m(Pages, {
|
||||||
base: '/page/' + this.data.page.path,
|
base: '/page/' + page.path,
|
||||||
total: this.data.total_articles,
|
total: this.data.total_articles,
|
||||||
page: this.currentPage,
|
page: this.currentPage,
|
||||||
}),
|
}),
|
||||||
|
@ -167,26 +182,46 @@ const Page = {
|
||||||
: null,
|
: null,
|
||||||
])
|
])
|
||||||
: this.data.articles.length
|
: this.data.articles.length
|
||||||
? m('aside.news.single', [
|
? m('aside.news.single',
|
||||||
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 + ':'),
|
page.pictureFallback
|
||||||
|
? m('a', {
|
||||||
|
rel: 'noopener',
|
||||||
|
href: page.media_path,
|
||||||
|
}, [
|
||||||
|
m('picture.page-cover', [
|
||||||
|
m('source', {
|
||||||
|
srcset: page.pictureAvif,
|
||||||
|
sizes: page.pictureCover,
|
||||||
|
type: 'image/avif',
|
||||||
|
}),
|
||||||
|
m('img', {
|
||||||
|
srcset: page.pictureJpeg,
|
||||||
|
sizes: page.pictureCover,
|
||||||
|
alt: 'Cover image for ' + page.name,
|
||||||
|
src: page.pictureFallback,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
m('h4', 'Latest posts under ' + page.name + ':'),
|
||||||
this.data.articles.map(function(article) {
|
this.data.articles.map(function(article) {
|
||||||
return m(Newsentry, { article: article })
|
return m(Newsentry, { article: article })
|
||||||
}),
|
}),
|
||||||
m(Pages, {
|
m(Pages, {
|
||||||
base: '/page/' + this.data.page.path,
|
base: '/page/' + page.path,
|
||||||
total: this.data.total_articles,
|
total: this.data.total_articles,
|
||||||
page: this.currentPage,
|
page: this.currentPage,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
: this.data.page.media
|
: page.media
|
||||||
? m('img.page-cover.single', { src: this.data.page.media.medium_url, alt: 'Cover image for ' + this.data.page.name } )
|
? m('img.page-cover.single', { src: page.media.medium_url, alt: 'Cover image for ' + page.name } )
|
||||||
: null,
|
: null,
|
||||||
]),
|
]),
|
||||||
Authentication.currentUser
|
Authentication.currentUser
|
||||||
? m('div.admin-actions', [
|
? m('div.admin-actions', [
|
||||||
m('span', 'Admin controls:'),
|
m('span', 'Admin controls:'),
|
||||||
m(m.route.Link, { href: '/admin/pages/' + this.data.page.path }, 'Edit page'),
|
m(m.route.Link, { href: '/admin/pages/' + page.path }, 'Edit page'),
|
||||||
])
|
])
|
||||||
: null,
|
: null,
|
||||||
])
|
])
|
17
nfp_moe_old/app/widgets/dialogue.js
Normal file
17
nfp_moe_old/app/widgets/dialogue.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
const Dialogue = {
|
||||||
|
view: function(vnode) {
|
||||||
|
return m('div.floating-container', {
|
||||||
|
hidden: vnode.attrs.hidden,
|
||||||
|
}, m('dialogue', [
|
||||||
|
m('h2', vnode.attrs.title),
|
||||||
|
m('p', vnode.attrs.message),
|
||||||
|
m('div.buttons', [
|
||||||
|
m('button', { class: vnode.attrs.yesclass || '', onclick: vnode.attrs.onyes }, vnode.attrs.yes),
|
||||||
|
m('button', { class: vnode.attrs.noclass || '', onclick: vnode.attrs.onno }, vnode.attrs.no),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Dialogue
|
61
nfp_moe_old/app/widgets/editorblock.js
Normal file
61
nfp_moe_old/app/widgets/editorblock.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
Blocks:
|
||||||
|
* Paragraph
|
||||||
|
* Header
|
||||||
|
* SimpleImage
|
||||||
|
* Quote
|
||||||
|
* CodeTool
|
||||||
|
* List
|
||||||
|
* Delimiter
|
||||||
|
* RawTool
|
||||||
|
|
||||||
|
Other:
|
||||||
|
* InlineCode
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EditorBlock = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.id = null
|
||||||
|
this.output = null
|
||||||
|
this.onbeforeupdate(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
onbeforeupdate: function(vnode) {
|
||||||
|
if (!vnode.attrs.block && !this.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (vnode.attrs.block && vnode.attrs.block.id
|
||||||
|
&& vnode.attrs.block.id === this.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vnode.attrs.block && vnode.attrs.block.id
|
||||||
|
&& vnode.attrs.block.id !== this.id) {
|
||||||
|
this.renderblock(vnode)
|
||||||
|
} else {
|
||||||
|
this.output = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderblock: function(vnode) {
|
||||||
|
let block = vnode.attrs.block
|
||||||
|
this.id = block.id
|
||||||
|
switch (block.type) {
|
||||||
|
case 'paragraph':
|
||||||
|
this.output = m('p', m.trust(block.data.text))
|
||||||
|
break
|
||||||
|
case 'htmlraw':
|
||||||
|
this.output = m.trust(block.data.html)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.output = m('p', m.trust(block))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return this.output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EditorBlock
|
64
nfp_moe_old/app/widgets/fileupload.js
Normal file
64
nfp_moe_old/app/widgets/fileupload.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
|
||||||
|
const FileUpload = {
|
||||||
|
fileChanged: function(vnode, event) {
|
||||||
|
if (!event.target.files[0]) return
|
||||||
|
|
||||||
|
let preview = null
|
||||||
|
if (event.target.files[0].type.startsWith('image')) {
|
||||||
|
preview = URL.createObjectURL(event.target.files[0])
|
||||||
|
}
|
||||||
|
if (this.preview) {
|
||||||
|
this.preview.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = {
|
||||||
|
file: event.target.files[0],
|
||||||
|
preview: preview,
|
||||||
|
clear: function() {
|
||||||
|
URL.revokeObjectURL(preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.preview = out
|
||||||
|
|
||||||
|
vnode.attrs.onfile(out)
|
||||||
|
},
|
||||||
|
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.loading = false
|
||||||
|
this.preview = null
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
let media = vnode.attrs.media
|
||||||
|
|
||||||
|
return m('fileupload', {
|
||||||
|
class: vnode.attrs.class || null,
|
||||||
|
}, [
|
||||||
|
this.preview || media
|
||||||
|
? vnode.attrs.useimg
|
||||||
|
? [ m('img', { src: this.preview && this.preview.preview || media }), m('div.showicon')]
|
||||||
|
: m('a.display.inside', {
|
||||||
|
href: this.preview && this.preview.preview || media,
|
||||||
|
style: {
|
||||||
|
'background-image': 'url("' + (this.preview && this.preview.preview || media) + '")',
|
||||||
|
},
|
||||||
|
}, m('div.showicon'))
|
||||||
|
: m('div.inside.showbordericon')
|
||||||
|
,
|
||||||
|
m('input', {
|
||||||
|
accept: 'image/*',
|
||||||
|
type: 'file',
|
||||||
|
onchange: this.fileChanged.bind(this, vnode),
|
||||||
|
}),
|
||||||
|
media && vnode.attrs.ondelete
|
||||||
|
? m('button.remove', { onclick: vnode.attrs.ondelete })
|
||||||
|
: null,
|
||||||
|
this.loading
|
||||||
|
? m('div.loading-spinner')
|
||||||
|
: null,
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FileUpload
|
1
nfp_moe_old/base
Symbolic link
1
nfp_moe_old/base
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../base
|
22
nfp_moe_old/dev.mjs
Normal file
22
nfp_moe_old/dev.mjs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import { ServiceCore } from 'service-core'
|
||||||
|
import * as index from './index.mjs'
|
||||||
|
|
||||||
|
var core = new ServiceCore('nfp_moe', import.meta.url, 4030, '')
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
frontend: {
|
||||||
|
url: 'http://localhost:4030'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = JSON.parse(fs.readFileSync('./config.json'))
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
config.port = 4030
|
||||||
|
|
||||||
|
core.setConfig(config)
|
||||||
|
core.init(index).then(function() {
|
||||||
|
return core.run()
|
||||||
|
})
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue