Finished initial release-worthy version, added appveyor and starting testing appveyor auto building
continuous-integration/appveyor/branch AppVeyor build failed Details

master
Jonatan Nilsson 2022-08-18 09:59:48 +00:00
parent 17882b457d
commit 71b1725655
199 changed files with 779 additions and 12287 deletions

View File

@ -1,48 +0,0 @@
version: 2
jobs:
build:
docker:
- image: docker:latest
environment:
- di: "nfpis/nfpmoe"
- dtag: "latest"
- service_name: "nfpmoe"
- target_port: "7030" # The public port
- service_port: "4030" # Container port
working_directory: ~/nfpmoe
steps:
- run:
name: Update and install SSH & Git & sed
command: apk update && apk upgrade && apk add --no-cache bash git openssh sed
- checkout
- setup_remote_docker
- run:
name: Replace version in config
command: |
sed -i "s/circleci_version_number/${CIRCLE_BUILD_NUM}/g" config/config.default.json
- run:
name: Build docker image
command: docker build -t ${di}:build_${CIRCLE_BUILD_NUM} -t ${di}:${CIRCLE_SHA1} -t ${di}:${dtag} .
- run:
name: Push to docker
command: |
docker login -u $DOCKER_USER -p $DOCKER_PASS
docker push ${di} --all-tags
- deploy:
name: Deploy to production
command: |
if [ "${CIRCLE_BRANCH}" != "master" ]; then
echo Not running on master. Exiting.
exit 0
fi
echo "$MASTER_HOST" | base64 -d > ~/.ssh/master_host
echo "$MASTER_KEY" | base64 -d > ~/.ssh/master_key
chmod 600 ~/.ssh/master_key
ssh -p 51120 -i ~/.ssh/master_key -o "UserKnownHostsFile ~/.ssh/master_host" root@212.30.212.2 "docker ${service_name} ${di}:build_${CIRCLE_BUILD_NUM} ${target_port} ${service_port}"
workflows:
version: 2
build_deploy:
jobs:
- build:
context: org-global

1
.npmrc
View File

@ -1 +0,0 @@
package-lock=false

View File

@ -1 +1 @@
# nfp_moe
# nfp_sites

82
appveyor.yml Normal file
View File

@ -0,0 +1,82 @@
# version format
version: '{build}'
deploy: on
# branches to build
branches:
# whitelist
only:
- master
# Do not build on tags (GitHub, Bitbucket, GitLab, Gitea)
skip_tags: true
# Maximum number of concurrent jobs for the project
max_jobs: 1
clone_depth: 1
# Build worker image (VM template)
build_cloud: Docker
environment:
docker_image: node:16-alpine
npm_config_cache: /appveyor/projects/cache
build_script:
- sh: |
chown -R node:node /appveyor/projects
chmod -R 777 /appveyor/projects
apk add curl jq
if [ $? -eq 0 ]; then
echo "Finished installling curl and jq"
else
exit 1
fi
for f in *; do
[ ! -d "$f" ] || [ -L "$f" ] || [ "$f" = "base" ] && continue;
echo "checking $f";
cd $f
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases for name ${CURR_NAME}"
curl -s -X GET -H "Authorization: token $deploytoken" https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases | grep -o "\"name\"\:\"${CURR_NAME}\"" > /dev/null
if [ $? -eq 0 ] ; then
echo "Skipping $f since $CURR_NAME already exists";
else
rm base
cp -Rf ../base ./base
npm install
npm run build
./7zas a -xr!*.xcf -mx9 "${CURR_VER}_build-sc.7z" package.json index.mjs api base public
echo "Creating release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
-H "Authorization: token $deploytoken" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases \
-d "{\"tag_name\":\"v${CURR_VER}\",\"name\":\"v${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${APPVEYOR_REPO_COMMIT} :\n\n${APPVEYOR_REPO_COMMIT_MESSAGE}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
echo "Adding ${CURR_VER}_build-sc.7z to release ${RELEASE_ID}"
curl \
-X POST \
-H "Authorization: token $deploytoken" \
-F "attachment=@${CURR_VER}_build-sc.7z" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases/$RELEASE_ID/assets
echo "Deplying to production"
MAN_PORT=$(cat package.json | jq -r .port)
MAN_NAME=$(cat package.json | jq -r .name)
Echo "curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME"
curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME
fi
cd..
done
# on build failure
on_failure:
- sh: echo on_failure

View File

@ -1,5 +1,5 @@
import { parseArticles, parseArticle } from './util.mjs'
import { uploadMedia, uploadFile } from '../media/upload.mjs'
import { uploadMedia, uploadFile, deleteFile } from '../media/upload.mjs'
import { mediaToDatabase } from '../media/util.mjs'
export default class ArticleRoutes {
@ -7,6 +7,7 @@ export default class ArticleRoutes {
Object.assign(this, {
uploadMedia: uploadMedia,
uploadFile: uploadFile,
deleteFile: deleteFile,
})
}
@ -69,7 +70,6 @@ export default class ArticleRoutes {
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
}
console.log(params)
let res = await ctx.db.safeCallProc('article_auth_get_update_create', params)
ctx.body = this.private_getUpdateArticle_resOutput(res)
@ -89,8 +89,6 @@ export default class ArticleRoutes {
/** PUT: /api/auth/articles/:id */
async auth_updateCreateSingleArticle(ctx) {
console.log(ctx.req.body)
let newBanner = null
let newMedia = null

View File

@ -3,7 +3,7 @@ import { HttpError } from 'flaska'
import { decode, encode } from '../util.mjs'
import config from '../config.mjs'
const levels = {
export const RankLevels = {
Normal: 1,
Manager: 10,
Admin: 100,
@ -12,7 +12,48 @@ const levels = {
const issuer = config.get('mssql:connectionUser')
const secret = config.get('jwtsecret')
export function authenticate(minLevel = levels.Manager) {
export function verifyValidToken(parts, minLevel) {
if (parts.length !== 4) {
throw new HttpError(401, 'Authentication token invalid')
}
const hmac = crypto.createHmac('sha256', secret)
hmac.update([parts[0], parts[1], parts[2]].join('.'))
let apiSignature = encode(hmac.digest())
if (apiSignature !== parts[3]) {
throw new HttpError(401, 'Authentication token invalid signature')
}
let header
let body
try {
header = JSON.parse(decode(parts[0]).toString('utf8'))
body = JSON.parse(decode(parts[1]).toString('utf8'))
} catch (err) {
throw new HttpError(401, 'Authentication token invalid json')
}
if (header.alg !== 'HS256') {
throw new HttpError(401, 'Authentication token invalid alg')
}
let unixNow = Math.floor(Date.now() / 1000)
// Validate token, add a little skew support for issued_at
if (body.iss !== issuer || !body.iat || !body.exp
|| body.iat > unixNow + 300 || body.exp <= unixNow) {
throw new HttpError(403, 'Authentication token expired or invalid')
}
if (body.rank < minLevel) {
throw new HttpError(401, 'User does not have access to this resource')
}
return body
}
export function authenticate(minLevel = RankLevels.Manager) {
return function(ctx) {
if (!ctx.req.headers.authorization) {
throw new HttpError(401, 'Authentication token missing')
@ -23,40 +64,7 @@ export function authenticate(minLevel = levels.Manager) {
let parts = ctx.req.headers.authorization.slice(7).split('.')
if (parts.length !== 4) {
throw new HttpError(401, 'Authentication token invalid')
}
const hmac = crypto.createHmac('sha256', secret)
const token = [parts[0], parts[1], parts[2]].join('.')
hmac.update(token)
let apiSignature = encode(hmac.digest())
if (apiSignature !== parts[3]) {
throw new HttpError(401, 'Authentication token invalid signature')
}
let header
let body
try {
header = JSON.parse(decode(parts[0]).toString('utf8'))
body = JSON.parse(decode(parts[1]).toString('utf8'))
} catch (err) {
throw new HttpError(401, 'Authentication token invalid json')
}
if (header.alg !== 'HS256') {
throw new HttpError(401, 'Authentication token invalid alg')
}
let unixNow = Math.floor(Date.now() / 1000)
// Validate token, add a little skew support for issued_at
if (body.iss !== issuer || !body.iat || !body.exp
|| body.iat > unixNow + 300 || body.exp <= unixNow) {
throw new HttpError(403, 'Authentication token expired or invalid')
}
ctx.state.auth_user = body
ctx.state.auth_token = token
ctx.state.auth_user = verifyValidToken(parts, minLevel)
ctx.state.auth_token = [parts[0], parts[1], parts[2]].join('.')
}
}

View File

@ -1,5 +1,6 @@
import _ from 'lodash'
import nconf from 'nconf-lite'
import Nconf from 'nconf-lite'
const nconf = new Nconf()
// Helper method for global usage.
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
@ -16,30 +17,15 @@ nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
nconf.env({
separator: '__',
whitelist: [
'DATABASE_URL',
'NODE_ENV',
'server__port',
'server__host',
'knex__connection__host',
'knex__connection__user',
'knex__connection__database',
'knex__connection__password',
'knex__connectionslave__host',
'knex__connectionslave__user',
'knex__connectionslave__database',
'knex__connectionslave__password',
'upload__baseurl',
'upload__port',
'upload__host',
'upload__name',
'upload__secret',
'bunyan__name',
'mssql__connectionString',
'media__secret',
'media__iss',
'media__path',
'media__filePath',
'media__removePath',
'frontend__url',
'jwt__secret',
'sessionsecret',
'bcrypt',
'name',
'NODE_VERSION',
'jwtsecret',
],
parseValues: true,
})
@ -67,6 +53,7 @@ nconf.defaults({
"iss": "dev",
"path": "https://media.nfp.is/media/resize",
"filePath": "https://media.nfp.is/media",
"removePath": "https://media.nfp.is/media/",
"preview": {
"out": "base64",
"format": "avif",
@ -171,14 +158,7 @@ nconf.defaults({
}
},
},
"fileSize": 524288000,
"upload": {
"baseurl": "https://cdn.nfp.is",
"port": "2111",
"host": "storage01.nfp.is",
"name": "nfpmoe-dev",
"secret": "nfpmoe-dev"
}
"fileSize": 524288000
})

View File

@ -10,31 +10,6 @@ export function initPool(core, config) {
core.log.info('MSSQL connection open')
})
let waiting = false
/*pool.on('error', function(error) {
if (error.length) {
let msg = 'Error in MSSQL pool\n => ' + error[0].message.trim()
for (let i = 1; i < error.length; i++) {
msg += '\n => ' + error[i].message.trim()
}
core.log.error(msg)
} else {
core.log.error('Error in MSSQL pool')
core.log.error(error)
}
if (waiting) { return }
core.log.warn('Attempting to connect again in 5 seconds')
waiting = true
setTimeout(function() {
waiting = false
console.log('opening')
pool.open()
console.log('done')
}, 5000)
})*/
core.log.info('Attempting to connect to MSSQL server')
pool.open()

View File

@ -48,10 +48,8 @@ export default class Client {
reject(err)
})
req.on('timeout', function() {
console.log("req.on('timeout')")
req.destroy()
let d2 = new Date()
console.log((d2 - d1))
reject(new Error(`Request ${method} ${path} timed out`))
})
req.on('response', res => {
@ -141,6 +139,16 @@ export default class Client {
},
})
}
delete(url, body) {
let parsed = JSON.stringify(body)
return this.customRequest('DELETE', url, parsed, {
headers: {
'Content-Type': 'application/json',
'Content-Length': parsed.length,
},
})
}
upload(url, files, method = 'POST', body = {}) {
const boundary = `---------${this.random(32)}`

View File

@ -71,4 +71,13 @@ export function uploadFile(file) {
type: file.type,
}
})
}
}
export function deleteFile(filename) {
const media = config.get('media')
const client = new Client()
let token = client.createJwt({ iss: media.iss }, media.secret)
return client.delete(media.removePath + filename + '?token=' + token, { })
}

View File

@ -4,6 +4,6 @@
"flaska": "^1.3.1",
"formidable": "^1.2.6",
"msnodesqlv8": "^2.4.7",
"nconf-lite": "^1.0.1"
"nconf-lite": "^2.0.0"
}
}

View File

@ -1,4 +1,4 @@
import { parsePage, parsePagesToTree } from './util.mjs'
import { parsePage, parsePages, parsePagesToTree } from './util.mjs'
import { uploadMedia, uploadFile } from '../media/upload.mjs'
import { parseArticle, parseArticles } from '../article/util.mjs'
import { mediaToDatabase } from '../media/util.mjs'
@ -36,13 +36,17 @@ export default class PageRoutes {
}
/** GET: /api/pages/[path] */
async getPage(ctx) {
async getPage(ctx, onlyReturn = false) {
let res = await ctx.db.safeCallProc('pages_get_single', [
ctx.params.path || null,
Math.max(ctx.query.get('page') || 1, 1),
Math.min(ctx.query.get('per_page') || 10, 25),
])
if (onlyReturn) {
return this.getPage_resOut(res)
}
ctx.body = this.getPage_resOut(res)
}
@ -61,7 +65,7 @@ export default class PageRoutes {
ctx.state.auth_token
])
ctx.body = parsePagesToTree(res.first)
ctx.body = parsePagesToTree(parsePages(res.first))
}
async private_getUpdatePage(ctx, body = null, banner = null, media = null) {
@ -80,7 +84,6 @@ export default class PageRoutes {
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
}
console.log(params)
let res = await ctx.db.safeCallProc('pages_auth_get_update_create', params)
let out = {
@ -98,8 +101,6 @@ export default class PageRoutes {
/** PUT: /api/auth/pages/:id */
async auth_updateCreateSinglePage(ctx) {
console.log(ctx.req.body)
let newBanner = null
let newMedia = null

View File

@ -24,6 +24,13 @@ export function parsePagesToTree(pages) {
}
}
export function parsePages(pages) {
for (let i = 0; i < pages.length; i++) {
parsePage(pages[i])
}
return pages
}
export function parsePage(page) {
if (!page) {
return null

View File

@ -3,21 +3,26 @@ import dot from 'dot'
import { FileResponse, HttpError } from 'flaska'
import fs from 'fs/promises'
import fsSync from 'fs'
import { RankLevels, verifyValidToken } from './authentication/security.mjs'
export default class ServeHandler {
constructor(opts = {}) {
Object.assign(this, {
pageRoutes: opts.pageRoutes,
fs: opts.fs || fs,
fsSync: opts.fsSync || fsSync,
root: opts.root,
template: null,
frontend: opts.frontend || 'http://localhost:4000',
version: opts.version || 'version',
})
Object.assign(this, opts)
if (!opts.fs) {
this.fs = fs
}
if (!opts.fsSync) {
this.fsSync = fsSync
}
if (!opts.frontend) {
this.frontend = 'http://localhost:4000'
}
if (!opts.version) {
this.version = 'version'
}
let indexFile = fsSync.readFileSync(path.join(this.root, 'index.html'))
this.template = dot.template(indexFile.toString(), { argName: ['headerDescription', 'headerImage', 'headerTitle', 'headerUrl', 'payloadData', 'payloadLinks', 'payloadTree', 'version', 'nonce'] })
this.template = dot.template(indexFile.toString(), { argName: ['headerDescription', 'headerImage', 'headerTitle', 'headerUrl', 'payloadData', 'payloadTree', 'version', 'nonce'] })
// console.log(indexFile.toString())
}
@ -28,23 +33,35 @@ export default class ServeHandler {
/** GET: /::file */
serve(ctx) {
if (ctx.params.file.startsWith('api/')) {
throw new HttpError(404, 'Not Found: ' + ctx.params.file, { status: 404, message: 'Not Found: ' + ctx.params.file })
return this.serveIndex(ctx)
}
let file = path.resolve(path.join(this.root, ctx.params.file ? ctx.params.file : 'index.html'))
if (!ctx.params.file || ctx.params.file === 'index.html') {
if (!ctx.params.file
|| ctx.params.file === 'index.html'
|| ctx.params.file.startsWith('/page')
|| ctx.params.file.startsWith('/article')
|| ctx.params.file.startsWith('/admin')) {
return this.serveIndex(ctx)
}
if (!file.startsWith(this.root)) {
ctx.status = 404
ctx.body = 'HTTP 404 Error'
return
return this.serveIndex(ctx)
}
if (file.indexOf('admin') >= 0
&& (file.indexOf('.js') >= 0 || file.indexOf('.css') >= 0)) {
verifyValidToken((ctx.query.get('token') || '').split('.'), RankLevels.Manager)
}
return this.fs.stat(file)
.then(function(stat) {
if (file.indexOf('admin') === -1) {
ctx.headers['Cache-Control'] = 'max-age=2592000'
} else {
ctx.headers['Cache-Control'] = 'no-store'
}
ctx.body = new FileResponse(file, stat)
})
.catch((err) => {
@ -56,25 +73,7 @@ export default class ServeHandler {
}
async serveIndex(ctx) {
let payload = {
headerDescription: 'Small fansubbing and scanlation group translating and encoding our favourite shows from Japan.',
headerImage: this.frontend + '/assets/img/heart.png',
headerTitle: 'NFP Moe - Anime/Manga translation group',
headerUrl: this.frontend + ctx.url,
payloadData: null,
payloadLinks: null,
payloadTree: null,
version: this.version,
nonce: ctx.state.nonce,
}
try {
payload.payloadTree = JSON.stringify(await this.pageRoutes.getPageTree(ctx, true))
} catch (e) {
ctx.log.error(e)
}
ctx.body = this.template(payload)
ctx.body = this.template({})
ctx.type = 'text/html; charset=utf-8'
}

View File

@ -51,7 +51,6 @@ export default class ArticleRoutes extends Parent {
}
}
let file = await this.uploadFile(ctx.req.files.file)
console.log(file)
let params = [
ctx.state.auth_token,
@ -79,10 +78,10 @@ export default class ArticleRoutes extends Parent {
null,
null,
ctx.params.fileId,
0,
1,
]
let res = await ctx.db.safeCallProc('article_auth_file_create_delete', params)
console.log(res)
await this.deleteFile(res.first[0].deleted_filename)
}
}

34
nfp_moe/api/serve.mjs Normal file
View File

@ -0,0 +1,34 @@
import Parent from '../base/serve.mjs'
export default class ServeHandler extends Parent {
async serveIndex(ctx) {
let payload = {
headerDescription: 'Small fansubbing and scanlation group translating and encoding our favourite shows from Japan.',
headerImage: this.frontend + '/assets/img/heart.png',
headerTitle: 'NFP Moe - Anime/Manga translation group',
headerUrl: this.frontend + ctx.url,
payloadData: null,
payloadTree: null,
version: this.version,
nonce: ctx.state.nonce,
}
try {
payload.payloadTree = JSON.stringify(await this.pageRoutes.getPageTree(ctx, true))
if (ctx.url === '/' || (ctx.url.startsWith('/page/') && ctx.url.lastIndexOf('/') === 5)) {
ctx.params.path = null
if (ctx.url.lastIndexOf('/') === 5) {
ctx.params.path = ctx.url.slice(ctx.url.lastIndexOf('/') + 1)
}
payload.payloadData = JSON.stringify(await this.pageRoutes.getPage(ctx, true))
}
console.log('url', ctx.url)
} catch (e) {
ctx.log.error(e)
}
ctx.body = this.template(payload)
ctx.type = 'text/html; charset=utf-8'
}
}

View File

@ -1,6 +1,6 @@
import config from '../base/config.mjs'
import Parent from '../base/server.mjs'
import ServeHandler from '../base/serve.mjs'
import ServeHandler from './serve.mjs'
import ArticleRoutes from './article_routes.mjs'
import PageRoutes from './page_routes.mjs'
@ -14,6 +14,7 @@ export default class Server extends Parent {
this.routes.page = new PageRoutes()
this.routes.serve = new ServeHandler({
pageRoutes: this.routes.page,
articleRoutes: this.routes.article,
root: localUtil.getPathFromRoot('../public'),
version: this.core.app.running,
frontend: config.get('frontend:url'),

View File

@ -24,6 +24,17 @@ const FileUpload = {
vnode.attrs.onfile(out)
},
fileRemoved: function(vnode) {
if (this.preview) {
this.preview.clear()
this.preview = null
vnode.attrs.onfile(null)
}
if (vnode.attrs.media) {
vnode.attrs.ondelete(vnode.attrs.media)
}
},
oninit: function(vnode) {
this.loading = false
this.preview = null
@ -66,10 +77,10 @@ const FileUpload = {
type: 'file',
onchange: this.fileChanged.bind(this, vnode),
}),
/*imageLink && vnode.attrs.ondelete
? m('button.remove', { onclick: vnode.attrs.ondelete })
imageLink && vnode.attrs.ondelete
? m('button.remove', { onclick: this.fileRemoved.bind(this, vnode), title: 'Remove image' }, m.trust('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M144 400C144 408.8 136.8 416 128 416C119.2 416 112 408.8 112 400V176C112 167.2 119.2 160 128 160C136.8 160 144 167.2 144 176V400zM240 400C240 408.8 232.8 416 224 416C215.2 416 208 408.8 208 400V176C208 167.2 215.2 160 224 160C232.8 160 240 167.2 240 176V400zM336 400C336 408.8 328.8 416 320 416C311.2 416 304 408.8 304 400V176C304 167.2 311.2 160 320 160C328.8 160 336 167.2 336 176V400zM310.1 22.56L336.9 64H432C440.8 64 448 71.16 448 80C448 88.84 440.8 96 432 96H416V432C416 476.2 380.2 512 336 512H112C67.82 512 32 476.2 32 432V96H16C7.164 96 0 88.84 0 80C0 71.16 7.164 64 16 64H111.1L137 22.56C145.8 8.526 161.2 0 177.7 0H270.3C286.8 0 302.2 8.526 310.1 22.56V22.56zM148.9 64H299.1L283.8 39.52C280.9 34.84 275.8 32 270.3 32H177.7C172.2 32 167.1 34.84 164.2 39.52L148.9 64zM64 432C64 458.5 85.49 480 112 480H336C362.5 480 384 458.5 384 432V96H64V432z"/></svg>'))
: null,
this.loading
/*this.loading
? m('div.loading-spinner')
: null,*/
])

View File

@ -108,6 +108,12 @@ const AdminArticles = {
? 'rowfeatured'
: ''
}, [
m('td.nopadding', article.banner_alt_prefix
? m('a', { href: article.banner_path, target: '_blank' }, m('img', { src: article.banner_alt_prefix + '_small.avif' }))
: m.trust('&nbsp;') ),
m('td.nopadding', article.media_alt_prefix
? m('a', { href: article.media_path, target: '_blank' }, m('img', { src: article.media_alt_prefix + '_small.avif' }))
: m.trust('&nbsp;') ),
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)),
@ -138,6 +144,8 @@ const AdminArticles = {
: m('table', [
m('thead',
m('tr', [
m('th', 'Banner'),
m('th', 'Cover'),
m('th', 'Title'),
m('th', 'Path'),
m('th', 'Page'),

View File

@ -19,6 +19,8 @@ const EditArticle = {
this.pages = [{id: null, name: 'Frontpage'}]
this.pages = this.pages.concat(PageTree.getFlatTree())
this.removeBanner = false
this.removeMedia = false
this.newBanner = null
this.newMedia = null
this.dateInstance = null
@ -93,7 +95,13 @@ const EditArticle = {
if (name === 'path') {
this.editedPath = true
} else if (name === 'name' && !this.editedPath) {
this.data.article.path = this.data.article.name.toLowerCase().replace(/ /g, '-')
this.data.article.path = this.data.article.name
.normalize("NFD").replace(/[\u0300-\u036f]/g, '')
.toLocaleLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/\-{2,}/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
}
},
@ -108,14 +116,20 @@ const EditArticle = {
mediaUploaded: function(type, file) {
if (type === 'banner') {
this.newBanner = file
this.removeBanner = false
} else {
this.newMedia = file
this.removeMedia = false
}
},
mediaRemoved: function(type) {
this.data.article['remove_' + type] = true
this.data.article[type + '_prefix'] = null
this.data.article[type + '_alt_prefix'] = null
if (type === 'banner') {
this.removeBanner = true
} else {
this.removeMedia = true
}
},
save: function(vnode, e) {
@ -146,8 +160,8 @@ const EditArticle = {
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)
formData.append('remove_banner', this.removeBanner ? true : false)
formData.append('remove_media', this.removeMedia ? true : false)
this.loading = true
@ -192,6 +206,9 @@ const EditArticle = {
},
refreshFiles: function(vnode, prom) {
this.loading = true
m.redraw()
prom.then(() => {
return api.sendRequest({
method: 'GET',
@ -204,21 +221,16 @@ const EditArticle = {
this.error = err.message
})
.then(() => {
this.loading = false
m.redraw()
})
},
confirmRemoveFile: function(vnode, file) {
console.log(file)
/*Dialogue.showDialogue(
'Delete file',
'Are you sure you want to remove "' + file.filename + '"',
'Delete',
'alert',
'Don\'t delete',
'',
page,
this.confirmRemovePage.bind(this, vnode))*/
return this.refreshFiles(vnode, api.sendRequest({
method: 'DELETE',
url: '/api/auth/articles/' + this.lastid + '/files/' + file.id,
}))
},
askConfirmRemoveFile: function(vnode, file) {

View File

@ -66,6 +66,12 @@ const AdminPages = {
drawPage: function(vnode, page) {
return [
m('tr', [
m('td.nopadding', page.banner_alt_prefix
? m('a', { href: page.banner_path, target: '_blank' }, m('img', { src: page.banner_alt_prefix + '_small.avif' }))
: m.trust('&nbsp;') ),
m('td.nopadding', page.media_alt_prefix
? m('a', { href: page.media_path, target: '_blank' }, m('img', { src: page.media_alt_prefix + '_small.avif' }))
: m.trust('&nbsp;') ),
m('td', [
page.parent_id ? m('span.subpage', ' - ') : null,
m(m.route.Link, { href: '/admin/pages/' + page.id }, page.name),
@ -97,6 +103,8 @@ const AdminPages = {
: m('table', [
m('thead',
m('tr', [
m('th', 'Banner'),
m('th', 'Cover'),
m('th', 'Title'),
m('th', 'Path'),
m('th.right', 'Updated'),

View File

@ -14,8 +14,8 @@ const Article = {
if (this.lastId !== article.id) {
this.lastId = article.id
let pictureCover = '(max-width: 639px) calc(100vw - 40px), '
+ '(max-width: 1000px) 300px, '
let pictureCover = '(max-width: 639px) calc(100vw - 10px), '
+ '(max-width: 1000px) calc(100vw - 265px), '
+ '400px'
if (vnode.attrs.full) {
pictureCover = '(max-width: 1280) calc(100vw - 2rem), '

View File

@ -9,29 +9,33 @@ const Footer = {
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)',
m('div.first'),
m('div.middle', [
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)',
]),
]),
m('div.asuna.spritesheet'),
]
},
}

View File

@ -54,7 +54,7 @@ const Menu = {
} else {
localStorage.removeItem(DarkModeStorageName)
}
document.body.className = (this.darkIsOn ? 'darkmode ' : 'daymode ')
document.body.className = (this.darkIsOn ? 'nightmode ' : 'daymode ')
+ (window.supportsavif ? 'avifsupport' : 'jpegonly')
},
@ -63,8 +63,11 @@ const Menu = {
m('header', [
m('div.inside', [
m(m.route.Link,
{ href: '/', class: 'logo' },
m('h1', 'NFP Moe')
{ href: '/', class: 'title' },
[
m('div.logo.spritesheet'),
m('h1', 'NFP Moe'),
]
),
m('aside', [
Authentication.currentUser
@ -77,7 +80,7 @@ const Menu = {
m(m.route.Link, { href: '/admin/articles/add' }, 'Create article'),
m(m.route.Link, { href: '/admin/articles' }, 'Articles'),
m(m.route.Link, { href: '/admin/pages' }, 'Pages'),
m(m.route.Link, { hidden: Authentication.currentUser.rank < 100, href: '/admin/staff' }, 'Staff'),
// m(m.route.Link, { hidden: Authentication.currentUser.rank < 100, href: '/admin/staff' }, 'Staff'),
])
]
: null,

View File

@ -6,6 +6,7 @@ const Footer = require('./footer')
const Login = require('./site_login')
const SitePage = require('./site_page')
const SiteArticle = require('./site_article')
const NotFoundView = require('./site_404')
window.m = m
m.route.setOrig = m.route.set
@ -29,6 +30,7 @@ const allRoutes = {
'/article/:id': SiteArticle,
'/admin/:path': AdminResolver,
'/admin/:path/:id': AdminResolver,
'/:404...': NotFoundView,
}
// Wait until we finish checking avif support, some views render immediately and will ask for this immediately before the callback gets called.

View File

@ -20,7 +20,7 @@ const Paginator = {
),
}, 'Previous'),
]
: m('div'),
: [ m('div'), m('div')],
m('div', 'Page ' + currentPage),
currentPage < maxPage
? [
@ -31,7 +31,7 @@ const Paginator = {
href: vnode.attrs.base + '?page=' + maxPage,
}, 'Last')
]
: m('div'),
: [ m('div'), m('div')],
])
},
}

20
nfp_moe/app/site_404.js Normal file
View File

@ -0,0 +1,20 @@
const m = require('mithril')
const NotFoundView = {
oninit: function(vnode) {
},
oncreate: function() {
},
view: function(vnode) {
return [
m('div.wrapper.not_found', [
m('h4.notfound', '404: Page, article or file was not found'),
m('div.asuna.spritesheet'),
]),
]
},
}
module.exports = NotFoundView

View File

@ -2,6 +2,7 @@ const m = require('mithril')
const Article = require('./article')
const api = require('./api')
const media = require('./media')
const NotFoundView = require('./site_404')
window.LoadComments = false
window.HyvorLoaded = false
@ -22,6 +23,7 @@ const SiteArticle = {
this.path = m.route.param('id')
this.data.article = window.__nfpdata
window.__nfpdata = null
this.afterData()
} else {
this.fetchArticle(vnode)
}
@ -58,10 +60,7 @@ const SiteArticle = {
})
.then((result) => {
this.data = result
if (!this.data.article) {
this.error = 'Article not found'
}
this.afterData()
}, (err) => {
this.error = err.message
})
@ -73,6 +72,12 @@ const SiteArticle = {
})
},
afterData: function() {
if (!this.data.article) {
this.error = 'Article not found'
}
},
view: function(vnode) {
let article = this.data.article
@ -80,13 +85,16 @@ const SiteArticle = {
this.loading
? m('div.loading-spinner')
: null,
!this.loading && this.error
!this.loading && this.error === 'Article not found'
? NotFoundView.view()
: null,
!this.loading && this.error && this.error !== 'Article not found'
? m('div.wrapper', m('div.error', {
onclick: () => {
this.error = ''
this.fetchPage(vnode)
},
}, 'Page error: ' + this.error + '. Click here to try again'))
}, 'Article error: ' + this.error + '. Click here to try again'))
: null,
(article
? m('.inside.vertical', [

View File

@ -59,13 +59,14 @@ const Login = {
return [
m('div.wrapper', [
this.loading ? m('div.loading-spinner') : null,
m('div.login--first'),
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('label', 'Email or name'),
m('input', {
type: 'text',
value: this.username,
@ -82,6 +83,7 @@ const Login = {
value: 'Log in',
}),
]),
m('div.login--asuna.spritesheet'),
]),
]
},

View File

@ -7,6 +7,7 @@ const Article = require('./article')
const Articleslim = require('./article_slim')
const media = require('./media')
const EditorBlock = require('./editorblock')
const NotFoundView = require('./site_404')
const ArticlesPerPage = 10
@ -25,12 +26,20 @@ const SitePage = {
this.children = []
this.currentPage = Number(m.route.param('page')) || 1
console.log('test', window.__nfpdata)
if (window.__nfpdata) {
this.path = m.route.param('id')
this.data = window.__nfpdata
this.lastpage = this.currentPage
this.data = window.__nfpdata
window.__nfpdata = null
window.__nfpsubdata = null
this.children = PageTree.Tree
if (this.path) {
this.children = PageTree.TreeMap.get(this.path)
this.children = this.children && this.children.children || []
}
this.afterData()
} else {
this.fetchPage(vnode)
}
@ -76,28 +85,7 @@ const SitePage = {
})
.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'
}
this.picture = media.generatePictureSource(this.data.page,
'(max-width: 840px) calc(100vw - 82px), '
+ '758px')
if (this.lastpage !== 1) {
document.title = 'Page ' + this.lastpage + ' - ' + title
} else {
document.title = title
}
this.afterData()
}, (err) => {
this.error = err.message
})
@ -109,6 +97,30 @@ const SitePage = {
})
},
afterData: function() {
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'
}
this.picture = media.generatePictureSource(this.data.page,
'(max-width: 840px) calc(100vw - 82px), '
+ '758px')
if (this.lastpage !== 1) {
document.title = 'Page ' + this.lastpage + ' - ' + title
} else {
document.title = title
}
},
view: function(vnode) {
let page = this.data.page
let featuredBanner = media.getBannerImage(this.data.featured, '/article/')
@ -118,7 +130,10 @@ const SitePage = {
this.loading
? m('div.loading-spinner')
: null,
!this.loading && this.error
!this.loading && this.error === 'Page not found'
? NotFoundView.view()
: null,
!this.loading && this.error && this.error !== 'Page not found'
? m('div.wrapper', m('div.error', {
onclick: () => {
this.error = ''
@ -163,52 +178,57 @@ const SitePage = {
m('h2.title', 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
? media.getArticlePicture(this.picture, false, page.media_path, 'Image for page ' + page.name)
: null),
(page && page.content
? m('div.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,
}),
]),
]),
(page || this.data.articles.length
? m('.inside', [
this.children.length
? m('aside', { class: page ? '' : 'frontpage' }, [
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),
]
}),
!page
? m('div.asuna.spritesheet')
: null,
])
: null,
m('div.container', [
(page
? media.getArticlePicture(this.picture, false, page.media_path, 'Image for page ' + page.name)
: null),
(page && page.content
? m('div.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,
}),
]),
])
: null),
])
},
}

View File

@ -1,7 +1,7 @@
import config from '../base/config.mjs'
export function start(http, port, ctx) {
config.stores.overrides.store = ctx.config
config.sources[1].store = ctx.config
return import('./api/server.mjs')
.then(function(module) {

View File

@ -1,6 +1,7 @@
{
"name": "nfp_moe",
"version": "2.0.0",
"port": 4110,
"description": "NFP Moe website",
"main": "index.js",
"directories": {
@ -55,7 +56,7 @@
"flaska": "^1.3.0",
"formidable": "^1.2.6",
"msnodesqlv8": "^2.4.7",
"nconf-lite": "^1.0.1"
"nconf-lite": "^2.0.0"
},
"devDependencies": {
"asbundle": "^2.6.1",

View File

@ -10,6 +10,15 @@
--admin-table-header-fg: #fff;
}
.nightmode {
--admin-bg: hsl(213.9, 100%, 20%);
--admin-bg-highlight: hsl(213.9, 100%, 35%);
--admin-color: #fff;
--admin-table-border: #01579b;
--admin-table-header-bg: #3D77C7;
--admin-table-header-fg: #fff;
}
/*
===================== main =====================
*/
@ -65,6 +74,18 @@ input[type=checkbox] {
padding: 0.5rem;
}
.admin table tbody td.nopadding {
padding: 0;
text-align: center;
width: 75px;
}
.admin table tbody td.nopadding img {
margin: 0;
max-width: 75px;
max-height: 75px;
}
.admin table thead th {
background-color: var(--admin-table-header-bg);
border: solid 1px var(--admin-table-border);
@ -75,7 +96,8 @@ input[type=checkbox] {
color: var(--alt-color);
}
.admin table tr:hover td {
.admin table tr:hover td,
.admin table tr.rowfeatured td {
background: var(--admin-bg-highlight);
}
@ -197,6 +219,16 @@ fileupload .text {
color: var(--seperator);
}
fileupload .remove {
width: 50px;
height: 50px;
position: absolute;
top: 0;
right: 0;
margin: 0;
z-index: 3;
}
fileupload input,
.fileupload input {
position: absolute;
@ -224,6 +256,7 @@ fileupload input,
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 4;
}
dialogue {

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="52px" height="52px" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve">
<g opacity="0.8">
<circle fill="#FFFFFF" cx="26" cy="26" r="25"/>
<path fill="#FF0000" d="M26,0C11.664,0,0,11.663,0,26s11.664,26,26,26c14.337,0,26-11.664,26-26S40.337,0,26,0z M26,50
C12.767,50,2,39.233,2,26C2,12.766,12.767,2,26,2s24,10.767,24,24C50,39.233,39.233,50,26,50z"/>
<path fill="#FF0000" d="M22.338,24.725c-0.549,0.055-0.95,0.545-0.896,1.095l0.875,8.75c0.052,0.516,0.486,0.9,0.994,0.9
c0.033,0,0.067-0.002,0.101-0.005c0.549-0.055,0.95-0.545,0.896-1.095l-0.875-8.75C23.378,25.071,22.89,24.669,22.338,24.725z"/>
<path fill="#FF0000" d="M29.662,24.725c-0.551-0.058-1.04,0.346-1.095,0.896l-0.875,8.75c-0.055,0.55,0.346,1.04,0.896,1.095
c0.034,0.003,0.067,0.005,0.101,0.005c0.508,0,0.942-0.385,0.994-0.9l0.875-8.75C30.612,25.27,30.212,24.78,29.662,24.725z"/>
<path fill="#FF0000" d="M35.562,15.154h-6.688v-0.625c0-1.585-1.29-2.875-2.875-2.875c-1.585,0-2.875,1.29-2.875,2.875v0.625
h-6.688c-1.654,0-3,1.346-3,3v1c0,1.449,1.033,2.661,2.401,2.939c-0.159,0.446-0.225,0.927-0.161,1.407l1.941,14.484
c0.188,1.395,1.334,2.446,2.665,2.446h11.436c1.328,0,2.472-1.052,2.661-2.447L36.323,23.5c0.063-0.48-0.003-0.96-0.161-1.406
c1.367-0.278,2.4-1.49,2.4-2.939v-1C38.562,16.5,37.217,15.154,35.562,15.154z M25.125,14.529c0-0.482,0.393-0.875,0.875-0.875
s0.875,0.393,0.875,0.875v0.625h-1.75V14.529z M32.397,37.718c-0.055,0.407-0.347,0.714-0.679,0.714H20.283
c-0.334,0-0.628-0.307-0.683-0.713l-1.94-14.483c-0.035-0.262,0.035-0.535,0.186-0.727c0.13-0.165,0.309-0.26,0.491-0.26h15.325
c0.184,0,0.364,0.095,0.491,0.255c0.152,0.195,0.223,0.469,0.188,0.731L32.397,37.718z M36.562,19.154c0,0.551-0.448,1-1,1H16.438
c-0.551,0-1-0.449-1-1v-1c0-0.551,0.449-1,1-1h19.125c0.552,0,1,0.449,1,1V19.154z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="52px" height="52px" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve">
<g opacity="0.8">
<g>
<circle fill="#FFFFFF" cx="26" cy="26" r="25"/>
<path fill="#f57c00" d="M26,52C11.664,52,0,40.337,0,26S11.664,0,26,0c14.336,0,26,11.663,26,26S40.336,52,26,52z M26,2
C12.767,2,2,12.767,2,26s10.767,24,24,24s24-10.767,24-24S39.233,2,26,2z"/>
</g>
<path fill="#f57c00" d="M37.939,36.936H13.785c-1.958,0-3.551-1.594-3.551-3.553v-8.875c0-0.553,0.448-1,1-1s1,0.447,1,1v8.875
c0,0.856,0.696,1.553,1.551,1.553h24.154c0.855,0,1.552-0.696,1.552-1.553V19.746c0-0.856-0.696-1.553-1.552-1.553h-8.559
c-0.553,0-1-0.447-1-1s0.447-1,1-1h8.559c1.959,0,3.552,1.594,3.552,3.553v13.637C41.491,35.342,39.898,36.936,37.939,36.936z"/>
<path fill="#f57c00" d="M11.234,25.508c-0.552,0-1-0.447-1-1v-4.762c0-1.959,1.593-3.553,3.551-3.553h8.533c0.552,0,1,0.447,1,1
s-0.448,1-1,1h-8.533c-0.855,0-1.551,0.696-1.551,1.553v4.762C12.234,25.061,11.787,25.508,11.234,25.508z"/>
<path fill="#f57c00" d="M33.977,43.328H17.748c-0.552,0-1-0.447-1-1v-1.439c0-1.332,1.083-2.416,2.415-2.416h13.398
c1.331,0,2.415,1.084,2.415,2.416v1.439C34.977,42.881,34.529,43.328,33.977,43.328z M18.748,41.328h14.229v-0.439
c0-0.229-0.186-0.416-0.415-0.416H19.163c-0.229,0-0.415,0.187-0.415,0.416V41.328z"/>
<g>
<path fill="#f57c00" d="M30.727,13.93c-0.293,0-0.584-0.128-0.781-0.375l-4.083-5.103l-4.083,5.103
c-0.345,0.433-0.975,0.501-1.406,0.156c-0.431-0.346-0.501-0.975-0.156-1.406l4.863-6.078c0.38-0.475,1.182-0.475,1.561,0
l4.863,6.078c0.346,0.432,0.275,1.061-0.155,1.406C31.166,13.858,30.945,13.93,30.727,13.93z"/>
<path fill="#f57c00" d="M25.862,28.311c-0.552,0-1-0.447-1-1V6.958c0-0.553,0.448-1,1-1s1,0.447,1,1v20.353
C26.862,27.863,26.415,28.311,25.862,28.311z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -17,19 +17,20 @@
<meta id="ogimageheight" property="og:image:height" content="500" />
{{? }}
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
<link rel="Stylesheet" href="/assets/app.css?v={{=version}}" type="text/css" />
<!--<link rel="icon" type="image/png" href="/assets/img/favicon.png">-->
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAdnJLH8AAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAriSURBVFjDVZZZjJ3nWcd/7/Kt5zv7ObOPt3jL2KnjhNhxY5ISt7S1iETFBSJISJXgCglxh1DvkLhCSKjihqtWIEAVEr0oSVEUtVCytMElmxXXGzO2x3MyY8929u/73oWLqazySM/Fc/d7H/2fVz/xzt/8kR8NChQeU5Q46zEOlpZn+XyjB97j0fS29ji0OIsxE8ADAhAoJZFK4qzD4/HWIaTk2LFTKBVibclGb43FhXnWVtc4KIG1lu7cArLerGN9SWemSbNVQ2qB8x4lNUpJtNZopRiMBkSRJokjwjBESIHzoAKNtYYwCsiyFB0G1OoNkoqm1mihtaYwE4SUtDtt6o06cRxSyVKG/R3k6uo6URwSBIo4iYiTCCEFRVFQSRNqtSoIj9YOpSVBqAhCjVIKBARaY6xBKYGUkigKaLZn0GFGkjVABDgnGQ6HGGuZTifEaUhWTXHSIDfvb1ONM3AeKQ8WZI1lsDd4MpeFoV2vMykmDIcDxsMRpQVnS7y1eOextsRaQ5LWyLIGQdpidzRmOCmwBnb3diiKkr3dPr4EhEdqjTbGo5XCOXfQ1pHnJbnO0YFGSkE+LYnChGI6JdYSrzRZq8PWw3s4a9BKURQlSkY0mh2yepOgWiOsKvLhACk1xpbgFfkkZzSaEqFRSiO3H28DDucszjmMMZSlxXlBURjy0jCe5NQaLYrC4KxHSk1WrRGogNF4grGeaW5odOapt2eJaw1EEKGikCSrkKQpne4spSmxFvb3h+ztDQGJbDVagH/yems9ZWmQUuE8lKUhTjJQEWXpmBSWRncWoWOMEwRRxl5/RKXeZXZhmaTZwesYdIAINCoMcR7StIL3Hmsc49GUyXiK8QKpUVhr8N5hncPDQbq1BqFxKNJ6i9FwgiciSOo0ZudQjRaFhSOnz5BU2zx99jzVzhwyruB1iNcaoRQqiRiNJwgPUiqM8+AF07ygtB7tpMc4gZTgnUcKSWEdQiuMk9zqbXPtxnV293boNpqcXTnJ8ctdBNA+/BSVuXlOxxn1uSVEGILWOKkYG4tUIeujPlYEFLlGioDCWUIh8dZgiil6VBhK6wkUeO+RUqKkYlTC/3x4HRnExGHCuNRs9kvu/teH6O4s3/itK5x78WXG+ZT5wx10koCSeCnZ2R/z3X/6Pj94400iHTFXzbj87IgzR2awVmHDALwBY5GdhUWKovzlB+WQSpDnJQ+3tkmSkGefOc3W9g5oibUejMAYRahi0kpGVm+Q1OoQhfgwhCjiO9/7N95648dUdR2J4qVXX+Vna4+409uhLB2q1i3T7iFvDcjzF19AKY0XAJ5Aa5QQCFtw8qklrPbk5RgtS7wq+NJXLvPFr7xCNNPEZxFxvYZOYlQcoqIYK2Nu37pJHCqkKMEb0iTi2XNn0bUM7z3B4vJfqfb8qi1BhxKEUEjhcAK01iwsLTAY7oMTDIY5y8eeohztE6mYfDIiqkSkSRWNRSAAg/cAmuGwYOv+PWqhwOmA5175MmdfuMT/rq0RTTaYOEPU6FyrVxK11Vv9M7117y7Oe7wXKKHxUlKtp1QqFUb9XXIR8NWv/SYfvfMuFSGJZpscO3qEQGuElAjvccaA9+Ad0luOH16i6jxWBnTrHY6dPsncfJUf/+MnOFfSnF8OUqm+vS70a7q/uwNhQL1aw3sHCqwraM4dYXO7RxCHXLn0BWq1AFsazl+6QBZK0AKEOoA3Am8s4Gk3Krz+zd/ns/d+ilSar1+9gvZjBg97jHtrNKp16q3u24ExhW7NjrTHsr6+zUy7hgoUSIdKEuZWzrG2/jlzLQWTLS4cmyGoNFBpCN4gkAhxkHp0AHmBcw4VlFy89Bxnji6gi5zcD0mtYvWDd6l4i3PeZNXa/tFTZ+xb//x3r2tsidKK8cRQiyRaWCaDAW++8Z9c/e3fwe/dJ4kj8AoRhkilDkLqASGQQkAU4QDpHEILzGRCoqEYTcEXjMqSva0NlBKUzg+PnjpjAZaOrxzTEoH2mt29Ae1uE2ct9Sjlje++yXBs+IM//F1EFZRziDCAJMYHAQKJA6xzaB1AJQDrwBikLhFhQJTGeDQ3bt7FCokXkkBJ/0srYeWFX39LWxezduM+95NdTp7ugA8JQkUmJb0f/IS/vbvKmVdf4OJLFzl08hBCCRySyThn9fYNdnoPqLQzOouzKB3gjWR+7jC+1kCGdcppyS8++ldiPAPrCJTT/ErptbUeg80JBVMmI0OSBoSRZLlToaIT7E6f3r+8zfd++B6/8Sff5NyFs+zurtPvPUDkfbKkpJ5aOtkUofqMJ2M+vf6AZusEGzce8sl7/0G2v4UMBd45nLPB/wMoC0eAJlKO/b2cShYTRR5lC5QOKAqLK0AMLN//9nfwf/wN5tsSsbcFsaY102Gwu8e94T5ZW1Otp7Tqhh/+w9+z/t+rLB6KIdR46fHe450PfxVAHj68RCw1s1mFx70+XgYEoWBcFgxtyW5RsjEa82A6QVAi9h+zf+cmZmeXYjAlTBTLR5dZWDxCJDqsftqjkgZ0W5K5UwtUrEL5A4/VWuPx8qOfvRs/ARj2x6SRph4HPFrbJi8cSkkclgCoCAk4RtMx5195Hp+PmIxHPM5H2BTqaYovC7TWfH7/IXsPNtm69ZhxOeTFq1dwURMUpHFEFIcorciLwjwBuH/nAWkUEAhD/vmAx5s7CKVozdfJVEw7i+kszfLK669x8twKBZKitEzKKdUw5Pp7H/Dxj37CzWufUNEx5Shn2NtDjwvqzRaLly4RdDJOrJwA5w8Ahrt/+QTgAOZARBId0d/sIxx0j8wjpMBIy6krX+L5q1fIZroYeyCvgS3Y+MUt8v0dvB2z07vNzU+uAQUIw8xMm7ERnH7pIt12izDSOAEoyXj7UfoEoN6tMXQlYyEZmILbN+5jSs/M0gwAuZSsvPwyC8dOYr2i3pkF6UnigADAHFixcKCdo8wdQVbHp01ay8dpzy4htGKcT2kvLBGFEXub668+AXBA2EzYRjDynt69PvuDkiCKsMJjvcYaT73TpTZ/iIlXhNU2MowRCoR0CGUQlGgEIkhY3+rTPfYCnYWjjLa3iULNKDfMnnoGqUPKR+srW5ubEkAqJ/BKIqKI0oO0EXfXHmADz+PxiDLL2NjcYn9vRHPhCE9fuspuOUeuZggrbWqNGbL6LGnSJZ9qhhOFSBbpHv4Cg+197tz8hEQrvA4edU6c/Ouo0STxY6bD7S6AlD5AOIlAYHKPIuDhw0dUqhX6+QDVaHL8/Hnurz1gd7tPlFZJ63NsTxLW+g0eixUmyTO8+9MN3n7zUx4NMpaffhFUglQhwnq08CghqHQX/qJ94pnPIiVZ//j9H5Vl3pEQ4ZzG4zBTgxae6e4I4SQ2lNTnl6k2m1TjlEer9/j5O+9w6+fX6K0/RndXcEtnaa9c4uGDCeUjhzQBi8dOklRq4D07Wxt4ARLXVWFFLaw8++cezfjeZyvjva1vSYHCekWRGwIs9VRSUZrd3X2yxRkqM12SNEUphZaC7V6POx9co5pmBK5gOtxn7dYaQgZ4CzKWRFGMx7G9sYkZuYMb0x5vRT67fPzfg9llyt1tNq9f+1OJF3gPZlzSSkO6tYO1DXd26MzNcPvOTZBQX5olSyrQHxPWOpy9eJnLX/0yX7zwHBeeP8/Cs2cIheL62++z9uHH3F+7y+jRFh+//z5xEBEI+RlQJlmj/LXXfu9bubHs3HqX/wOlxCTHo9yMUAAAAABJRU5ErkJggg==">
<link rel="Stylesheet" href="/assets/app.css?v=1" type="text/css" />
<link rel="preconnect" href="https://cdn.nfp.is" />
</head>
<body class="daymode">
<script type="text/javascript" nonce="{{=nonce}}">
if (localStorage.getItem('nfp_sites_darkmode')) {document.body.className = 'darkmode';}
if (localStorage.getItem('nfp_sites_darkmode')) {document.body.className = 'nightmode';}
window.__nfptree = {{=payloadTree}};
window.__nfpdata = {{=payloadData}};
</script>
<div id="header"></div>
<main id="main"></main>
<footer id="footer"></footer>
<script type="text/javascript" src="/assets/app.js?v={{=version}}"></script>
<script type="text/javascript" src="/assets/app.js?v=1"></script>
</body>
</html>

View File

@ -1 +0,0 @@
package-lock=false

View File

@ -1,18 +0,0 @@
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'),
}))
}
}

View File

@ -1,67 +0,0 @@
$primary-bg: #01579b;
$primary-fg: white;
$primary-fg-url: #FFC7C7;
$primary-light-bg: #3D77C7; // #4f83cc;
$primary-light-fg: white;
$primary-dark-bg: #002f6c;
$primary-dark-fg: white;
$secondary-bg: #f57c00;
$secondary-fg: black;
$secondary-light-bg: #ffad42;
$secondary-light-fg: black;
$secondary-dark-bg: #bb4d00;
$secondary-dark-fg: white;
$table-fg: #333;
$border: #ccc;
$border-fg: black;
$border-fg-url: #8f3c00;
$title-fg: #555;
$meta-fg: #757575; // #999
$meta-light-fg: #999999;
$main-bg: white;
$main-fg: black;
$input-bg: white;
$input-border: #333;
$input-fg: black;
$newsitem-bg: transparent;
$newsitem-border: none;
/* Dark theme */
$dark_primary-bg: #013b68;
$dark_primary-fg: white;
$dark_primary-fg-url: #FFC7C7;
$dark_primary-light-bg: #28518B;
$dark_primary-light-fg: white;
$dark_primary-dark-bg: #002f6c;
$dark_primary-dark-fg: white;
$dark_secondary-bg: #e05e00;
$dark_secondary-fg: black;
$dark_secondary-light-bg: #ffad42;
$dark_secondary-light-fg: black;
$dark_secondary-dark-bg: #e05e00;
$dark_secondary-dark-fg: white;
$dark_secondary-darker-fg: #fe791b;
$dark_table-fg: #333;
$dark_border: #343536;
$dark_border-fg: #d7dadc;;
$dark_border-fg-url: #e05e00;
$dark_title-fg: #808080;
$dark_meta-fg: hsl(0, 0%, 55%);
$dark_meta-light-fg: #999999;
$dark_main-bg: black;
$dark_main-fg: #d7dadc;
$dark_input-bg: #272729;
$dark_input-border: #343536;
$dark_input-fg: white;
$dark_newsitem-bg: #1a1a1b;
$dark_newsitem-border: 1px solid #343536;

View File

@ -1,12 +0,0 @@
const EditPage = require('./admin/editpage')
const AdminPages = require('./admin/pages')
const AdminArticles = require('./admin/articles')
const EditArticle = require('./admin/editarticle')
const AdminStaffList = require('./admin/stafflist')
const EditStaff = require('./admin/editstaff')
window.adminRoutes = {
pages: [AdminPages, EditPage],
articles: [AdminArticles, EditArticle],
staff: [AdminStaffList, EditStaff],
}

View File

@ -1,120 +0,0 @@
@import './_common';
.error {
font-size: 0.8em;
color: $secondary-dark-bg;
font-weight: bold;
padding-bottom: 20px;
}
$bordercolor: $primary-bg;
$headcolor: $primary-light-bg;
$headtext: $primary-light-fg;
.admin-wrapper table {
width: calc(100% - 20px);
margin: 10px;
border: solid 1px $bordercolor;
border-collapse: collapse;
border-spacing: 0;
font-size: 0.8em;
position: relative;
thead th {
background-color: $headcolor;
border: solid 1px $bordercolor;
color: $headtext;
padding: 10px;
text-align: left;
}
tbody td {
text-align: left;
border: solid 1px $bordercolor;
color: $table-fg;
padding: 10px;
}
a,
a:visited,
a:hover {
text-decoration: none;
color: $secondary-dark-bg;
font-weight: bold;
}
button {
color: $secondary-dark-bg;
background: transparent;
border: 1px solid $secondary-dark-bg;
}
td.right,
th.right {
text-align: right;
}
}
.floating-container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #00000099;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.input-group {}
.input-row {
display: flex;
& > * {
margin-right: 1rem;
flex: 2 1 auto;
}
& > .small {
flex: 0 0 auto;
}
& > *:last-child {
margin-right: 0;
}
}
.admin-wrapper .loading-spinner {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #00000066;
z-index: 1000;
}
@import 'admin/admin';
@import 'widgets/admin';
.codex-editor {
margin-bottom: 0.5rem;
}
input[type=checkbox] {
display: block;
height: 20px;
margin: 0.5rem 0;
width: 20px;
}
.darkmodeon {
.maincontainer .admin-wrapper {
color: $main-fg;
}
.error {
color: $dark_secondary-dark-bg;
}
}

View File

@ -1,149 +0,0 @@
.admin-wrapper {
flex-grow: 2;
display: flex;
flex-direction: column;
background: $primary-bg;
padding: 0 20px 50px;
}
.admin-actions {
background: $primary-bg;
display: flex;
justify-content: center;
min-height: 37px;
span {
color: $primary-fg;
padding: 10px;
font-size: 14px;
font-weight: bold;
}
a {
padding: 10px;
text-decoration: none;
color: $secondary-light-bg;
font-size: 14px;
font-weight: bold;
}
}
.fr-box,
.fr-toolbar,
.fr-box .second-toolbar {
border-radius: 0 !important;
}
@import 'pages';
@import 'articles';
@import 'staff';
.date-selector-wrapper {
width: 200px;
padding: 3px;
background-color: #fff;
box-shadow: 1px 1px 10px 1px #5c5c5c;
position: absolute;
font-size: 12px;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
z-index: 10;
/* user-select: none; */
}
.cal-header, .cal-row {
display: flex;
width: 100%;
height: 30px;
line-height: 30px;
text-align: center;
}
.cal-cell, .cal-nav {
cursor: pointer;
}
.cal-day-names {
height: 25px;
line-height: 25px;
}
.cal-day-names .cal-cell {
cursor: default;
font-weight: bold;
}
.cal-cell-prev, .cal-cell-next {
color: #777;
}
.cal-months .cal-row, .cal-years .cal-row {
height: 60px;
line-height: 60px;
}
.cal-nav-prev, .cal-nav-next {
flex: 0.15;
}
.cal-nav-current {
flex: 0.75;
font-weight: bold;
}
.cal-months .cal-cell, .cal-years .cal-cell {
flex: 0.25;
}
.cal-days .cal-cell {
flex: 0.143;
}
.cal-value {
color: #fff;
background-color: #286090;
}
.cal-cell:hover, .cal-nav:hover {
background-color: #eee;
}
.cal-value:hover {
background-color: #204d74;
}
/* time footer */
.cal-time {
display: flex;
justify-content: flex-start;
height: 27px;
line-height: 27px;
}
.cal-time-label, .cal-time-value {
flex: 0.12;
text-align: center;
}
.cal-time-slider {
flex: 0.77;
background-image: linear-gradient(to right, #d1d8dd, #d1d8dd);
background-repeat: no-repeat;
background-size: 100% 1px;
background-position: left 50%;
height: 100%;
}
.cal-time-slider input {
width: 100%;
-webkit-appearance: none;
background: 0 0;
cursor: pointer;
height: 100%;
outline: 0;
user-select: auto;
}
.ce-block__content,
.ce-toolbar__content { max-width:calc(100% - 120px) !important; }
.cdx-block { max-width: 100% !important; }
.codex-editor {
border: 1px solid var(--input-border);
background: var(--input-bg);
color: var(--input-fg);
}
.codex-editor:hover,
.codex-editor:active {
border-color: var(--secondary-bg);
}

View File

@ -1,162 +0,0 @@
const Article = require('../api/article')
const pagination = require('../api/pagination')
const Dialogue = require('../widgets/dialogue')
const Pages = require('../widgets/pages')
const common = require('../api/common')
const AdminArticles = {
oninit: function(vnode) {
this.error = ''
this.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 common.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.page_path }, article.page_name)),
m('td', m(m.route.Link, { href: '/article/' + article.path }, '/article/' + article.path)),
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.admin-wrapper', [
m('div.admin-actions', [
m('span', 'Actions:'),
m(m.route.Link, { href: '/admin/articles/add' }, 'Create new article'),
]),
m('article.editarticle', [
m('header', m('h1', 'All articles')),
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', 'Page'),
m('th', 'Path'),
m('th.right', 'Publish'),
m('th.right', 'By'),
m('th.right', 'Actions'),
])
),
m('tbody', this.data.articles.map((article) => this.drawArticle(vnode, article))),
],
),
/*m(Pages, {
base: '/admin/articles',
links: this.links,
}),*/
]),
]),
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

View File

@ -1,131 +0,0 @@
article.editarticle {
background: white;
padding: 0 0 20px;
header {
text-align: center;
padding: 10px;
background: $secondary-bg;
h1 {
color: $secondary-fg;
}
a {
font-size: 14px;
text-decoration: none;
margin-left: 10px;
color: $secondary-light-fg;
}
}
fileupload {
margin: 0 0 20px;
.inside {
height: 150px;
}
}
fileupload.cover {
align-self: center;
min-width: 178px;
}
form {
padding: 0 2rem 1rem;
textarea {
height: 300px;
}
label.slim {
font-size: 0.7em;
}
input.slim {
font-size: 0.8em;
padding: 2px 5px;
}
.loading-spinner {
height: 300px;
position: relative;
}
button.submit {
margin-left: 20px;
}
}
table {
tr.rowhidden td {
background: #e6e6e6;
}
tr.rowfeatured td {
background: hsl(120, 60%, 85%);
}
}
h5 {
margin-bottom: 20px;
}
& > .loading-spinner {
width: 240px;
height: 50px;
position: relative;
&.full {
width: 100%;
}
}
.fileupload {
align-self: flex-start;
padding: 0.5rem;
margin: 0.5rem 0 0.5rem 2rem;
min-width: 150px;
border: none;
border: 1px solid $secondary-bg;
background: $secondary-light-bg;
color: $secondary-light-fg;
position: relative;
text-align: center;
input {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0.01;
width: 100%;
cursor: pointer;
text-indent: -9999px;
z-index: 2;
}
}
files {
align-items: stretch;
width: 100%;
display: flex;
flex-direction: column;
padding: 1rem 2rem 0;
text-align: left;
h4 {
font-size: 1.1em;
font-weight: bold;
padding: 0 5px 5px;
margin-bottom: 10px;
border-bottom: 1px solid $border;
}
}
}
table span.subarticle {
padding: 0 5px;
}

View File

@ -1,950 +0,0 @@
(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 = "&lt;";
row.children[2].innerHTML = "&gt;";
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},
}),
},
});
})();

View File

@ -1,348 +0,0 @@
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

View File

@ -1,47 +0,0 @@
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

View File

@ -1,262 +0,0 @@
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

View File

@ -1,152 +0,0 @@
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

View File

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

View File

@ -1,113 +0,0 @@
const Page = require('../api/page.p')
const Dialogue = require('../widgets/dialogue')
const common = require('../api/common')
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 common.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 common.sendRequest({
method: 'DELETE',
url: '/api/auth/pages/' + removingPage.id,
})
.then(() => Page.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 [
(this.loading ?
m('div.loading-spinner')
: m('div.admin-wrapper', [
m('div.admin-actions', [
m('span', 'Actions:'),
m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'),
]),
m('article.editpage', [
m('header', m('h1', 'All pages')),
m('div.error', {
hidden: !this.error,
onclick: () => { this.fetchPages(vnode) },
}, this.error),
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(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

View File

@ -1,66 +0,0 @@
article.editpage {
text-align: center;
background: white;
padding: 0 0 20px;
header {
padding: 10px;
background: $secondary-bg;
h1 {
color: $secondary-fg;
}
a {
font-size: 14px;
text-decoration: none;
margin-left: 10px;
color: $secondary-light-fg;
}
}
fileupload {
margin: 0 0 20px;
.inside {
height: 150px;
}
}
fileupload.cover {
align-self: center;
min-width: 178px;
.display {
background-size: auto 100%;
}
}
form {
padding: 0 40px 20px;
textarea {
height: 300px;
}
.loading-spinner {
height: 300px;
position: relative;
}
}
h5 {
margin-bottom: 20px;
}
& > .loading-spinner {
width: 240px;
height: 50px;
position: relative;
}
}
table span.subpage {
padding: 0 5px;
}

View File

@ -1,49 +0,0 @@
article.editstaff {
text-align: center;
background: white;
padding: 0 0 20px;
header {
padding: 10px;
background: $secondary-bg;
h1 {
color: $secondary-fg;
}
a {
font-size: 14px;
text-decoration: none;
margin-left: 10px;
color: $secondary-light-fg;
}
}
form {
padding: 0 40px 20px;
textarea {
height: 300px;
}
.loading-spinner {
height: 300px;
position: relative;
}
}
h5 {
margin-bottom: 20px;
}
& > .loading-spinner {
width: 240px;
height: 50px;
position: relative;
&.full {
width: 100%;
}
}
}

View File

@ -1,110 +0,0 @@
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

View File

@ -1,64 +0,0 @@
const common = require('./common')
exports.createArticle = function(body) {
return common.sendRequest({
method: 'POST',
url: '/api/articles',
body: body,
})
}
exports.updateArticle = function(id, body) {
return common.sendRequest({
method: 'PUT',
url: '/api/articles/' + id,
body: body,
})
}
exports.getAllArticles = function() {
return common.sendRequest({
method: 'GET',
url: '/api/articles?includes=parent',
})
}
exports.getAllArticlesPagination = function(options) {
let extra = ''
if (options.sort) {
extra += '&sort=' + options.sort
}
if (options.per_page) {
extra += '&perPage=' + options.per_page
}
if (options.page) {
extra += '&page=' + options.page
}
if (options.includes) {
extra += '&includes=' + options.includes.join(',')
}
return '/api/articles?' + extra
}
exports.getAllPageArticles = function(pageId, includes) {
return common.sendRequest({
method: 'GET',
url: '/api/pages/' + pageId + '/articles?includes=' + includes.join(','),
})
}
exports.getArticle = function(id) {
return common.sendRequest({
method: 'GET',
url: '/api/articles/' + id + '?includes=media,parent,banner,files',
})
}
exports.removeArticle = function(article, id) {
return common.sendRequest({
method: 'DELETE',
url: '/api/articles/' + id,
})
}

View File

@ -1,8 +0,0 @@
const common = require('./common')
exports.getArticle = function(id) {
return common.sendRequest({
method: 'GET',
url: '/api/articles/' + id,
})
}

View File

@ -1,53 +0,0 @@
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)
})
}

View File

@ -1,12 +0,0 @@
const common = require('./common')
exports.uploadFile = function(articleId, file) {
let formData = new FormData()
formData.append('file', file)
return common.sendRequest({
method: 'POST',
url: '/api/articles/' + articleId + '/file',
body: formData,
})
}

View File

@ -1,17 +0,0 @@
const common = require('./common')
exports.uploadMedia = function(file, height) {
let formData = new FormData()
formData.append('file', file)
let extra = ''
if (height) {
extra = '?height=' + height
}
return common.sendRequest({
method: 'POST',
url: '/api/media' + extra,
body: formData,
})
}

View File

@ -1,114 +0,0 @@
const common = require('./common')
const Tree = window.__nfptree && window.__nfptree.tree || []
exports.Tree = Tree
exports.createPage = function(body) {
return common.sendRequest({
method: 'POST',
url: '/api/pages',
body: body,
}).then(function(res) {
res.children = []
if (!res.parent_id) {
Tree.push(res)
} else {
for (let i = 0; i < Tree.length; i++) {
if (Tree[i].id === res.parent_id) {
Tree[i].children.push(res)
break
}
}
}
return res
})
}
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.getTree = function() {
return common.sendRequest({
method: 'GET',
url: '/api/pages?tree=true&includes=children&fields=id,name,path,children(id,name,path)',
})
}
exports.updatePage = function(id, body) {
return common.sendRequest({
method: 'PUT',
url: '/api/pages/' + id,
body: body,
}).then(function(res) {
for (let i = 0; i < Tree.length; i++) {
if (Tree[i].id === res.id) {
res.children = Tree[i].children
Tree[i] = res
break
} else if (Tree[i].id === res.parent_id) {
for (let x = 0; x < Tree[i].children.length; x++) {
if (Tree[i].children[x].id === res.id) {
res.children = Tree[i].children[x].children
Tree[i].children[x] = res
break
}
}
break
}
}
if (!res.children) {
res.children = []
}
return res
})
}
exports.getAllPages = function() {
return common.sendRequest({
method: 'GET',
url: '/api/pages',
})
}
exports.getPage = function(id) {
return common.sendRequest({
method: 'GET',
url: '/api/pages/' + id + '?includes=media,banner',
})
}
exports.removePage = function(page, id) {
return common.sendRequest({
method: 'DELETE',
url: '/api/pages/' + id,
}).then(function() {
for (let i = 0; i < Tree.length; i++) {
if (Tree[i].id === page.id) {
Tree.splice(i, 1)
break
} else if (Tree[i].id === page.parent_id) {
for (let x = 0; x < Tree[i].children.length; x++) {
if (Tree[i].children[x].id === page.id) {
Tree[i].children.splice(x, 1)
break
}
}
break
}
}
return null
})
}

View File

@ -1,54 +0,0 @@
const common = require('./common')
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.getPage = function(path, page) {
return common.sendRequest({
method: 'GET',
url: '/api/' + (path ? 'pages/' + path : 'frontpage') + '?page=' + (page || 1),
})
}
exports.refreshTree = function() {
return common.sendRequest({
method: 'GET',
url: '/api/pagetree',
})
.then(pages => {
Tree.splice(0, Tree.length)
Tree.push.apply(Tree, pages.tree)
TreeMap.clear()
parseLeaf(Tree)
})
}

View File

@ -1,81 +0,0 @@
const common = require('./common')
function hasRel(x) {
return x && x.rel;
}
function intoRels (acc, x) {
function splitRel (rel) {
acc[rel] = xtend(x, { rel: rel });
}
x.rel.split(/\s+/).forEach(splitRel);
return acc;
}
function createObjects (acc, p) {
// rel="next" => 1: rel 2: next
var m = p.match(/\s*(.+)\s*=\s*"?([^"]+)"?/)
if (m) acc[m[1]] = m[2];
return acc;
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
function extend() {
var target = {}
for (var i = 0; i < arguments.length; i++) {
var source = arguments[i]
for (var key in source) {
if (hasOwnProperty.call(source, key)) {
target[key] = source[key]
}
}
}
return target
}
function parseLink(link) {
try {
var m = link.match(/<?([^>]*)>(.*)/)
, linkUrl = m[1]
, parts = m[2].split(';')
, qry = new URL(linkUrl).searchParams;
parts.shift();
var info = parts
.reduce(createObjects, {});
info = extend(qry, info);
info.url = linkUrl;
return info;
} catch (e) {
return null;
}
}
function parse(linkHeader) {
return linkHeader.split(/,\s*</)
.map(parseLink)
.filter(hasRel)
.reduce(intoRels, {});
}
exports.fetchPage = function(url) {
return common.sendRequest({
method: 'GET',
url: url,
}, true)
.then(function(result) {
return {
data: result.data,
links: parse(result.headers.link || ''),
total: Number(result.headers.pagination_total || '0'),
}
})
}

View File

@ -1,38 +0,0 @@
const common = require('./common')
exports.createStaff = function(body) {
return common.sendRequest({
method: 'POST',
url: '/api/staff',
body: body,
})
}
exports.updateStaff = function(id, body) {
return common.sendRequest({
method: 'PUT',
url: '/api/staff/' + id,
body: body,
})
}
exports.getAllStaff = function() {
return common.sendRequest({
method: 'GET',
url: '/api/staff',
})
}
exports.getStaff = function(id) {
return common.sendRequest({
method: 'GET',
url: '/api/staff/' + id,
})
}
exports.removeStaff = function(id) {
return common.sendRequest({
method: 'DELETE',
url: '/api/staff/' + id,
})
}

View File

@ -1,346 +0,0 @@
@import './_common';
html {
box-sizing: border-box;
font-size: 16px;
height: 100%;
}
*, *:before, *:after {
box-sizing: inherit;
}
body, h1, h2, h3, h4, h5, h6, p {
margin: 0;
padding: 0;
font-weight: normal;
}
body {
min-height: 100%;
font-family: Arial, Helvetica, sans-serif;
}
button {
font-family: Arial, Helvetica, sans-serif;
}
ol, ul {
list-style: none;
}
img {
max-width: 100%;
height: auto;
display: block;
}
@keyframes spinner-loader {
to {transform: rotate(360deg);}
}
.loading-spinner:before {
content: '';
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin-top: -10px;
margin-left: -10px;
border-radius: 50%;
border: 2px solid #ccc;
border-top-color: #333;
animation: spinner-loader .6s linear infinite;
z-index: 1000;
}
.maincontainer {
background: $main-bg;
color: $main-fg;
width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
width: 100%;
display: flex;
flex-direction: column;
}
main {
display: flex;
flex-direction: column;
flex-grow: 2;
position: relative;
}
.error {
font-size: 0.8em;
color: $secondary-dark-bg;
font-weight: bold;
padding-bottom: 20px;
}
.error-wrapper {
flex-grow: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: $border;
padding: 40px 0;
.error {
border: 2px dashed $secondary-dark-bg;
padding: 10px 20px;
font-size: 1em;
}
}
[hidden] { display: none !important; }
article {
display: flex;
flex-direction: column;
flex-grow: 2;
padding: 20px;
header {
display: flex;
flex-direction: column;
margin-bottom: 20px;
h1 {
font-size: 1.4em;
color: $title-fg;
flex-grow: 2;
}
span {
font-size: 0.8em;
color: $meta-fg;
}
}
.content {
display: flex;
flex-direction: column;
h5 {
font-size: 1.0em;
font-weight: bold;
color: $title-fg;
}
}
}
form {
display: flex;
flex-direction: column;
align-items: stretch;
label {
text-align: left;
font-weight: bold;
font-size: 0.8em;
padding: 5px 0 3px;
}
input[type=text],
input[type=password],
input[type=datetime-local],
select,
textarea {
width: 100%;
font-size: 1em;
padding: 0.5em;
margin: 0 0 0.5em;
background: $input-bg;
border: 1px solid $input-border;
color: $input-fg;
outline: none;
&:hover,
&:focus {
border-color: $secondary-bg;
}
}
input[type=submit],
button.submit {
align-self: center;
padding: 0.5em;
margin: 0.5em 0;
min-width: 150px;
border: 1px solid $secondary-bg;
background: $secondary-light-bg;
color: $secondary-light-fg;
height: 31px;
}
button.submit::-moz-focus-inner {
border: 0;
}
}
a, button {
outline: none;
}
button::-moz-focus-inner,
input[type="button"]::-moz-focus-inner,
input[type="submit"]::-moz-focus-inner,
input[type="reset"]::-moz-focus-inner {
padding: 0 !important;
border: 0 none !important;
}
@import 'editor';
@import 'menu/menu';
@import 'footer/footer';
@import 'login/login';
@import 'widgets/common';
@import 'pages/page';
@import 'article/article';
@import 'frontpage/frontpage';
.darkmodeon {
.maincontainer {
background: $dark_main-bg;
color: $dark_main-fg;
}
.error {
color: $dark_secondary-dark-bg;
}
.fr-view blockquote {
border-left-color: $dark_main-fg;
color: $dark_main-fg;
}
article.article,
article.login,
article.page {
header {
h1 {
// color: $dark_title-fg;
}
span {
color: $dark_meta-fg;
}
}
.content {
h5 {
color: $dark_title-fg;
}
}
}
.login form {
input[type=text],
input[type=password],
select,
textarea {
background: $dark_input-bg;
border: 1px solid $dark_input-border;
color: $dark_input-fg;
&:hover,
&:focus {
border-color: $dark_secondary-bg;
}
}
input[type=submit] {
border: 1px solid $dark_secondary-bg;
background: $dark_secondary-light-bg;
color: $dark_secondary-light-fg;
&:hover {
background: $dark_secondary-dark-bg;
color: $dark_secondary-dark-fg;
}
}
}
.fr-view {
a { color: $dark_secondary-dark-bg; }
}
}
:root {
--primary-bg: #01579b;
--primary-fg: white;
--primary-fg-url: #FFC7C7;
--primary-light-bg: #3D77C7; // #4f83cc;
--primary-light-fg: white;
--primary-dark-bg: #002f6c;
--primary-dark-fg: white;
--secondary-bg: #f57c00;
--secondary-fg: black;
--secondary-light-bg: #ffad42;
--secondary-light-fg: black;
--secondary-dark-bg: #bb4d00;
--secondary-dark-fg: white;
--table-fg: #333;
--border: #ccc;
--border-fg: black;
--border-fg-url: #8f3c00;
--title-fg: #555;
--meta-fg: #757575; // #999
--meta-light-fg: #999999;
--main-bg: white;
--main-fg: black;
--input-bg: white;
--input-border: #333;
--input-fg: black;
--newsitem-bg: transparent;
--newsitem-border: none;
}
.darkmodeon {
--primary-bg: #013b68;
--primary-fg: white;
--primary-fg-url: #FFC7C7;
--primary-light-bg: #28518B;
--primary-light-fg: white;
--primary-dark-bg: #002f6c;
--primary-dark-fg: white;
--secondary-bg: #e05e00;
--secondary-fg: black;
--secondary-light-bg: #ffad42;
--secondary-light-fg: black;
--secondary-dark-bg: #e05e00;
--secondary-dark-fg: white;
--secondary-darker-fg: #fe791b;
--table-fg: #333;
--border: #343536;
--border-fg: #d7dadc;;
--border-fg-url: #e05e00;
--title-fg: #808080;
--meta-fg: hsl(0, 0%, 55%);
--meta-light-fg: #999999;
--main-bg: black;
--main-fg: #d7dadc;
--input-bg: #272729;
--input-border: #343536;
--input-fg: white;
--newsitem-bg: #1a1a1b;
--newsitem-border: 1px solid #343536;
}

View File

@ -1,199 +0,0 @@
const m = require('mithril')
const ApiArticle = require('../api/article.p')
const Authentication = require('../authentication')
const Fileinfo = require('../widgets/fileinfo')
const EditorBlock = require('../widgets/editorblock')
const Article = {
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
}
ApiArticle.getArticle(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
return (
this.loading ?
m('article.article', m('div.loading-spinner'))
: this.error
? m('div.error-wrapper', m('div.error', {
onclick: function() {
vnode.state.error = ''
vnode.state.fetchArticle(vnode)
},
}, 'Article error: ' + this.error))
: m('article.article', [
article.page_path
? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name)])
: null,
m('header', m('h1', article.name)),
m('.fr-view', [
article.pictureFallback
? m('a.cover', {
rel: 'noopener',
href: article.media_path,
}, [
m('picture', [
m('source', {
srcset: article.pictureAvif,
sizes: article.pictureCover,
type: 'image/avif',
}),
m('img', {
srcset: article.pictureJpeg,
sizes: article.pictureCover,
alt: 'Image for news item ' + article.name,
src: article.pictureFallback,
}),
]),
])
: null,
article.content.blocks.map(block => {
return m(EditorBlock, { block: block })
}),
this.data.files.map(function(file) {
return m(Fileinfo, { file: file })
}),
m('div.entrymeta', [
'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'),
]),
]),
Authentication.currentUser
? m('div.admin-actions', [
m('span', 'Admin controls:'),
m(m.route.Link, { href: '/admin/articles/' + article.path }, 'Edit article'),
])
: null,
this.showcomments
? m('div.commentcontainer', [
m('div#disqus_thread', { oncreate: function() {
let fullhost = window.location.protocol + '//' + window.location.host
/*eslint-disable */
window.disqus_config = function () {
this.page.url = fullhost + '/article/' + vnode.state.article.path
this.page.identifier = 'article-' + vnode.state.article.id
};
(function() { // DON'T EDIT BELOW THIS LINE
var d = document, s = d.createElement('script');
s.src = 'https://nfp-moe.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})()
/*eslint-enable */
}}, m('div.loading-spinner')),
])
: m('button.opencomments', {
onclick: function() { vnode.state.showcomments = true },
}, 'Open comment discussion'),
])
)
},
}
module.exports = Article
/*
<div id="disqus_thread"></div>
<script>
/**
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS.
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables*/
/*
var disqus_config = function () {
this.page.url = PAGE_URL; // Replace PAGE_URL with your page's canonical URL variable
this.page.identifier = PAGE_IDENTIFIER; // Replace PAGE_IDENTIFIER with your page's unique identifier variable
};
/
(function() { // DON'T EDIT BELOW THIS LINE
var d = document, s = d.createElement('script');
s.src = 'https://nfp-moe.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
*/

View File

@ -1,151 +0,0 @@
article.article {
padding: 0;
header {
text-align: center;
margin: 20px 20px 0;
padding: 10px;
background: $secondary-bg;
width: 100%;
max-width: 1920px;
align-self: center;
h1 {
color: $secondary-fg;
}
}
.loading-spinner {
position: relative;
flex-grow: 2;
height: 300px;
}
.cover {
margin: 0 0 20px;
}
.admin-actions {
margin-bottom: 20px;
}
.fr-view {
margin: 0 20px;
padding: 20px 20px 10px;
width: calc(100% - 40px);
max-width: 800px;
flex: 2 0 0;
align-self: center;
background: $newsitem-bg;
border-right: 1px solid #343536;
border-left: 1px solid #343536;
a.cover img {
margin-bottom: 30px;
}
main {
padding: 0 5px;
}
}
fileinfo {
font-size: 0.8em;
ul {
padding-left: 0;
}
}
.opencomments {
border: none;
align-self: center;
width: calc(100% - 40px);
max-width: 300px;
background: transparent;
font-size: 1.2em;
color: $secondary-dark-bg;
cursor: pointer;
height: 50px;
margin: 0 0 30px;
}
.commentcontainer {
align-self: center;
width: calc(100% - 40px);
max-width: 800px;
margin-bottom: 30px;
min-height: 50px;
position: relative;
}
.goback {
width: calc(100% - 40px);
max-width: 800px;
align-self: center;
padding: 30px 5px 0;
margin-bottom: -10px;
a {
font-weight: bold;
text-decoration: none;
color: $secondary-dark-bg;
}
}
.entrymeta {
flex-grow: 2;
font-size: 11px;
color: $meta-fg;
font-weight: bold;
margin: 40px 0 0;
a {
color: $secondary-dark-bg;
margin: 0 4px;
text-decoration: none;
}
}
}
@media screen and (max-width: 639px){
article.article {
.fr-view {
margin: 0;
width: 100%;
border: none !important;
}
}
}
.darkmodeon {
article.article {
header {
background: $dark_secondary-bg;
h1 {
color: $dark_secondary-fg;
}
}
.fr-view {
background: $dark_newsitem-bg;
}
.opencomments {
color: $dark_secondary-dark-bg;
}
.goback a {
color: $dark_secondary-dark-bg;
}
.entrymeta {
color: $dark_meta-fg;
a {
color: $dark_secondary-dark-bg;
}
}
}
}

View File

@ -1,42 +0,0 @@
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

Some files were not shown because too many files have changed in this diff Show More