Finished initial release-worthy version, added appveyor and starting testing appveyor auto building
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
|
@ -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
|
@ -1 +0,0 @@
|
|||
package-lock=false
|
|
@ -1 +1 @@
|
|||
# nfp_moe
|
||||
# nfp_sites
|
82
appveyor.yml
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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('.')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
||||
|
||||
|
|
25
base/db.mjs
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)}`
|
||||
|
|
|
@ -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, { })
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
|
|
|
@ -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,*/
|
||||
])
|
||||
|
|
|
@ -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(' ') ),
|
||||
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(' ') ),
|
||||
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'),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(' ') ),
|
||||
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(' ') ),
|
||||
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'),
|
||||
|
|
|
@ -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), '
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
@ -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
|
|
@ -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', [
|
||||
|
|
|
@ -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'),
|
||||
]),
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
BIN
nfp_moe/public/assets/img/404_day_src.xcf
Normal file
BIN
nfp_moe/public/assets/img/404_night_src.xcf
Normal file
BIN
nfp_moe/public/assets/img/404_src.xcf
Normal file
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 437 KiB |
Before Width: | Height: | Size: 285 KiB |
BIN
nfp_moe/public/assets/img/asuna_login_day_src.xcf
Normal file
BIN
nfp_moe/public/assets/img/asuna_login_night_src.xcf
Normal file
BIN
nfp_moe/public/assets/img/combined.avif
Normal file
BIN
nfp_moe/public/assets/img/combined.png
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
nfp_moe/public/assets/img/combined.xcf
Normal file
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 345 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 10 KiB |
|
@ -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 |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 436 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 7.7 KiB |
|
@ -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 |
|
@ -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>
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
package-lock=false
|
|
@ -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'),
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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],
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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 = "<";
|
||||
row.children[2].innerHTML = ">";
|
||||
Object.defineProperty(this.el, "header", { value: row });
|
||||
tryAppendChild(row, this.el.wrapper);
|
||||
}
|
||||
this.setHeaderContent();
|
||||
}
|
||||
DTBox.prototype.setHeaderContent = function () {
|
||||
var content = this.year;
|
||||
if ("DAYS" == this.bodyType) {
|
||||
content = this.month_long + " " + content;
|
||||
} else if ("YEARS" == this.bodyType) {
|
||||
var start = this.year + 10 - (this.year % 10);
|
||||
content = start - 10 + "-" + (start - 1);
|
||||
}
|
||||
this.el.header.children[1].innerText = content;
|
||||
}
|
||||
DTBox.prototype.setupBody = function () {
|
||||
if (!this.el.body) {
|
||||
var el = document.createElement("div");
|
||||
el.classList.add("cal-body");
|
||||
Object.defineProperty(this.el, "body", { value: el });
|
||||
tryAppendChild(el, this.el.wrapper);
|
||||
}
|
||||
var toAppend = null;
|
||||
function makeGrid(rows, cols, className, firstRowClass, clickHandler) {
|
||||
var grid = document.createElement("div");
|
||||
grid.classList.add(className);
|
||||
for (var i = 1; i < rows + 1; i++) {
|
||||
var row = document.createElement("div");
|
||||
row.classList.add("cal-row", "cal-row-" + i);
|
||||
if (i == 1 && firstRowClass) {
|
||||
row.classList.add(firstRowClass);
|
||||
}
|
||||
for (var j = 1; j < cols + 1; j++) {
|
||||
var col = document.createElement("div");
|
||||
col.classList.add("cal-cell", "cal-col-" + j);
|
||||
col.onclick = clickHandler;
|
||||
row.appendChild(col);
|
||||
}
|
||||
grid.appendChild(row);
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
if ("DAYS" == this.bodyType) {
|
||||
toAppend = this.el.body.calDays;
|
||||
if (!toAppend) {
|
||||
toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this));
|
||||
for (var i = 0; i < 7; i++) {
|
||||
var cell = toAppend.children[0].children[i];
|
||||
cell.innerText = this.settings.config.weekdaysShort[i];
|
||||
cell.onclick = null;
|
||||
}
|
||||
this.el.body.calDays = toAppend;
|
||||
}
|
||||
} else if ("MONTHS" == this.bodyType) {
|
||||
toAppend = this.el.body.calMonths;
|
||||
if (!toAppend) {
|
||||
toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this));
|
||||
for (var i = 0; i < 3; i++) {
|
||||
for (var j = 0; j < 4; j++) {
|
||||
var monthShort = this.settings.config.monthsShort[4 * i + j];
|
||||
toAppend.children[i].children[j].innerText = monthShort;
|
||||
}
|
||||
}
|
||||
this.el.body.calMonths = toAppend;
|
||||
}
|
||||
} else if ("YEARS" == this.bodyType) {
|
||||
toAppend = this.el.body.calYears;
|
||||
if (!toAppend) {
|
||||
toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this));
|
||||
this.el.body.calYears = toAppend;
|
||||
}
|
||||
}
|
||||
empty(this.el.body);
|
||||
tryAppendChild(toAppend, this.el.body);
|
||||
this.setBodyContent();
|
||||
}
|
||||
DTBox.prototype.setBodyContent = function () {
|
||||
var grid = this.el.body.children[0];
|
||||
var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"];
|
||||
if ("DAYS" == this.bodyType) {
|
||||
var oneDayMilliSecs = 24 * 60 * 60 * 1000;
|
||||
var start = new Date(this.year, this.month, 1);
|
||||
var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay());
|
||||
|
||||
grid.children[6].style.display = "";
|
||||
for (var i = 1; i < 7; i++) {
|
||||
for (var j = 0; j < 7; j++) {
|
||||
var cell = grid.children[i].children[j];
|
||||
var month = adjusted.getMonth();
|
||||
var date = adjusted.getDate();
|
||||
|
||||
cell.innerText = date;
|
||||
cell.classList.remove(classes[0], classes[1], classes[2]);
|
||||
if (month != this.month) {
|
||||
if (i == 6 && j == 0) {
|
||||
grid.children[6].style.display = "none";
|
||||
break;
|
||||
}
|
||||
cell.classList.add(month < this.month ? classes[0] : classes[1]);
|
||||
} else if (isEqualDate(adjusted, this.value)){
|
||||
cell.classList.add(classes[2]);
|
||||
}
|
||||
adjusted = new Date(adjusted.getTime() + oneDayMilliSecs);
|
||||
}
|
||||
}
|
||||
} else if ("YEARS" == this.bodyType) {
|
||||
var year = this.year - (this.year % 10) - 1;
|
||||
for (i = 0; i < 3; i++) {
|
||||
for (j = 0; j < 4; j++) {
|
||||
grid.children[i].children[j].innerText = year;
|
||||
year += 1;
|
||||
}
|
||||
}
|
||||
grid.children[0].children[0].classList.add(classes[0]);
|
||||
grid.children[2].children[3].classList.add(classes[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {Event} e */
|
||||
DTBox.prototype.onTimeChange = function(e) {
|
||||
e.stopPropagation();
|
||||
if (e.type == 'mousedown') {
|
||||
this.cancelBlur += 1;
|
||||
return;
|
||||
}
|
||||
if (e.type == 'mouseup') {
|
||||
var self = this;
|
||||
setTimeout(function(){
|
||||
self.elem.focus();
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
var el = e.target;
|
||||
this[el.name] = parseInt(el.value) || 0;
|
||||
this.setupFooter();
|
||||
this.setInputValue();
|
||||
}
|
||||
|
||||
DTBox.prototype.setupFooter = function() {
|
||||
if (!this.el.footer) {
|
||||
var footer = document.createElement("div");
|
||||
var handler = this.onTimeChange.bind(this);
|
||||
var self = this;
|
||||
|
||||
function makeRow(label, name, range, changeHandler) {
|
||||
var row = document.createElement("div");
|
||||
row.classList.add('cal-time');
|
||||
|
||||
var labelCol = row.appendChild(document.createElement("div"));
|
||||
labelCol.classList.add('cal-time-label');
|
||||
labelCol.innerText = label;
|
||||
|
||||
var valueCol = row.appendChild(document.createElement("div"));
|
||||
valueCol.classList.add('cal-time-value');
|
||||
valueCol.innerText = '00';
|
||||
|
||||
var inputCol = row.appendChild(document.createElement("div"));
|
||||
var slider = inputCol.appendChild(document.createElement("input"));
|
||||
Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'});
|
||||
Object.defineProperty(footer, name, {value: slider});
|
||||
inputCol.classList.add('cal-time-slider');
|
||||
slider.onchange = changeHandler;
|
||||
slider.oninput = changeHandler;
|
||||
slider.onmousedown = changeHandler;
|
||||
slider.onmouseup = changeHandler;
|
||||
self[name] = self[name] || parseInt(slider.value) || 0;
|
||||
footer.appendChild(row)
|
||||
}
|
||||
makeRow(this.settings.config.timeDescr[0], 'hours', 23, handler);
|
||||
makeRow(this.settings.config.timeDescr[1], 'minutes', 59, handler);
|
||||
if (this.settings.config.showSeconds) {
|
||||
makeRow(this.settings.config.timeDescr[2], 'seconds', 59, handler);
|
||||
}
|
||||
|
||||
footer.classList.add("cal-footer");
|
||||
Object.defineProperty(this.el, "footer", { value: footer });
|
||||
tryAppendChild(footer, this.el.wrapper);
|
||||
}
|
||||
this.setFooterContent();
|
||||
}
|
||||
|
||||
DTBox.prototype.setFooterContent = function() {
|
||||
if (this.el.footer) {
|
||||
var footer = this.el.footer;
|
||||
footer.hours.value = this.hours;
|
||||
footer.children[0].children[1].innerText = padded(this.hours, 2);
|
||||
footer.minutes.value = this.minutes;
|
||||
footer.children[1].children[1].innerText = padded(this.minutes, 2);
|
||||
if (this.settings.config.showSeconds) {
|
||||
footer.seconds.value = this.seconds;
|
||||
footer.children[2].children[1].innerText = padded(this.seconds, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DTBox.prototype.setInputValue = function() {
|
||||
var date = new Date(this.year, this.month, this.day);
|
||||
var strings = [];
|
||||
if (this.settings.config.showDate) {
|
||||
strings.push(renderDate(date, this.settings));
|
||||
}
|
||||
if (this.settings.config.showTime) {
|
||||
var joined = new Date(date.getTime() + this.time);
|
||||
strings.push(renderTime(joined, this.settings));
|
||||
}
|
||||
this.elem.value = strings.join(', ');
|
||||
}
|
||||
|
||||
DTBox.prototype.onDateSelected = function (e) {
|
||||
var row = e.target.parentNode;
|
||||
var date = parseInt(e.target.innerText);
|
||||
if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) {
|
||||
this.month += 1;
|
||||
} else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) {
|
||||
this.month -= 1;
|
||||
}
|
||||
this.day = parseInt(e.target.innerText);
|
||||
this.value = new Date(this.year, this.month, this.day);
|
||||
this.setInputValue();
|
||||
this.setHeaderContent();
|
||||
this.setBodyContent();
|
||||
}
|
||||
|
||||
/** @param {Event} e */
|
||||
DTBox.prototype.onMonthSelected = function (e) {
|
||||
var col = 0;
|
||||
var row = 2;
|
||||
var cell = e.target;
|
||||
if (cell.parentNode.nextSibling){
|
||||
row = cell.parentNode.previousSibling ? 1: 0;
|
||||
}
|
||||
if (cell.previousSibling) {
|
||||
col = 3;
|
||||
if (cell.nextSibling) {
|
||||
col = cell.previousSibling.previousSibling ? 2 : 1;
|
||||
}
|
||||
}
|
||||
this.month = 4 * row + col;
|
||||
this.bodyType = "DAYS";
|
||||
this.setHeaderContent();
|
||||
this.setupBody();
|
||||
}
|
||||
|
||||
/** @param {Event} e */
|
||||
DTBox.prototype.onYearSelected = function (e) {
|
||||
this.year = parseInt(e.target.innerText);
|
||||
this.bodyType = "MONTHS";
|
||||
this.setHeaderContent();
|
||||
this.setupBody();
|
||||
}
|
||||
|
||||
/** @param {Event} e */
|
||||
DTBox.prototype.onHeaderChange = function (e) {
|
||||
var cell = e.target;
|
||||
if (cell.previousSibling && cell.nextSibling) {
|
||||
var idx = BODYTYPES.indexOf(this.bodyType);
|
||||
if (idx < 0 || !BODYTYPES[idx + 1]) {
|
||||
return;
|
||||
}
|
||||
this.bodyType = BODYTYPES[idx + 1];
|
||||
this.setupBody();
|
||||
} else {
|
||||
var sign = cell.previousSibling ? 1 : -1;
|
||||
switch (this.bodyType) {
|
||||
case "DAYS":
|
||||
this.month += sign * 1;
|
||||
break;
|
||||
case "MONTHS":
|
||||
this.year += sign * 1;
|
||||
break;
|
||||
case "YEARS":
|
||||
this.year += sign * 10;
|
||||
}
|
||||
if (this.month > 11 || this.month < 0) {
|
||||
this.year += Math.floor(this.month / 11);
|
||||
this.month = this.month > 11 ? 0 : 11;
|
||||
}
|
||||
}
|
||||
this.setHeaderContent();
|
||||
this.setBodyContent();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elem
|
||||
* @returns {{left:number, top:number}}
|
||||
*/
|
||||
function getOffset(elem) {
|
||||
var box = elem.getBoundingClientRect();
|
||||
var left = window.pageXOffset !== undefined ? window.pageXOffset :
|
||||
(document.documentElement || document.body.parentNode || document.body).scrollLeft;
|
||||
var top = window.pageYOffset !== undefined ? window.pageYOffset :
|
||||
(document.documentElement || document.body.parentNode || document.body).scrollTop;
|
||||
return { left: box.left + left, top: box.top + top };
|
||||
}
|
||||
function empty(e) {
|
||||
for (; e.children.length; ) e.removeChild(e.children[0]);
|
||||
}
|
||||
function tryAppendChild(newChild, refNode) {
|
||||
try {
|
||||
refNode.appendChild(newChild);
|
||||
return newChild;
|
||||
} catch (e) {
|
||||
console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** @class */
|
||||
function hookFuncs() {
|
||||
/** @type {Handlers} */
|
||||
this._funcs = {};
|
||||
}
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Function} func
|
||||
*/
|
||||
hookFuncs.prototype.add = function(key, func){
|
||||
if (!this._funcs[key]){
|
||||
this._funcs[key] = [];
|
||||
}
|
||||
this._funcs[key].push(func)
|
||||
}
|
||||
/**
|
||||
* @param {String} key
|
||||
* @returns {Function[]} handlers
|
||||
*/
|
||||
hookFuncs.prototype.get = function(key){
|
||||
return this._funcs[key] ? this._funcs[key] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array.<string>} arr
|
||||
* @param {String} string
|
||||
* @returns {Array.<string>} sorted string
|
||||
*/
|
||||
function sortByStringIndex(arr, string) {
|
||||
return arr.sort(function(a, b){
|
||||
var h = string.indexOf(a);
|
||||
var l = string.indexOf(b);
|
||||
var rank = 0;
|
||||
if (h < l) {
|
||||
rank = -1;
|
||||
} else if (l < h) {
|
||||
rank = 1;
|
||||
} else if (a.length > b.length) {
|
||||
rank = -1;
|
||||
} else if (b.length > a.length) {
|
||||
rank = 1;
|
||||
}
|
||||
return rank;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove keys from array that are not in format
|
||||
* @param {string[]} keys
|
||||
* @param {string} format
|
||||
* @returns {string[]} new filtered array
|
||||
*/
|
||||
function filterFormatKeys(keys, format) {
|
||||
var out = [];
|
||||
var formatIdx = 0;
|
||||
for (var i = 0; i<keys.length; i++) {
|
||||
var key = keys[i];
|
||||
if (format.slice(formatIdx).indexOf(key) > -1) {
|
||||
formatIdx += key.length;
|
||||
out.push(key);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {StringNumObj} FormatObj
|
||||
* @param {string} value
|
||||
* @param {string} format
|
||||
* @param {FormatObj} formatObj
|
||||
* @param {function(Object.<string, hookFuncs>): null} setHooks
|
||||
* @returns {FormatObj} formatObj
|
||||
*/
|
||||
function parseData(value, format, formatObj, setHooks) {
|
||||
var hooks = {
|
||||
canSkip: new hookFuncs(),
|
||||
updateValue: new hookFuncs(),
|
||||
}
|
||||
var keys = sortByStringIndex(Object.keys(formatObj), format);
|
||||
var filterdKeys = filterFormatKeys(keys, format);
|
||||
var vstart = 0; // value start
|
||||
if (setHooks) {
|
||||
setHooks(hooks);
|
||||
}
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
var fstart = format.indexOf(key);
|
||||
var _vstart = vstart; // next value start
|
||||
var val = null;
|
||||
var canSkip = false;
|
||||
var funcs = hooks.canSkip.get(key);
|
||||
|
||||
vstart = vstart || fstart;
|
||||
|
||||
for (var j = 0; j < funcs.length; j++) {
|
||||
if (funcs[j](formatObj)){
|
||||
canSkip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fstart > -1 && !canSkip) {
|
||||
var sep = null;
|
||||
var stop = vstart + key.length;
|
||||
var fnext = -1;
|
||||
var nextKeyIdx = i + 1;
|
||||
_vstart += key.length; // set next value start if current key is found
|
||||
|
||||
// get next format token used to determine separator
|
||||
while (fnext == -1 && nextKeyIdx < keys.length){
|
||||
var nextKey = keys[nextKeyIdx];
|
||||
nextKeyIdx += 1;
|
||||
if (filterdKeys.indexOf(nextKey) === -1) {
|
||||
continue;
|
||||
}
|
||||
fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start
|
||||
}
|
||||
if (fnext > -1){
|
||||
sep = format.slice(stop, fnext);
|
||||
if (sep) {
|
||||
var _stop = value.slice(vstart).indexOf(sep);
|
||||
if (_stop && _stop > -1){
|
||||
stop = _stop + vstart;
|
||||
_vstart = stop + sep.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
val = parseInt(value.slice(vstart, stop));
|
||||
|
||||
var funcs = hooks.updateValue.get(key);
|
||||
for (var k = 0; k < funcs.length; k++) {
|
||||
val = funcs[k](val, formatObj, vstart, stop);
|
||||
}
|
||||
}
|
||||
formatObj[key] = { index: vstart, value: val };
|
||||
vstart = _vstart; // set next value start
|
||||
}
|
||||
return formatObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} value
|
||||
* @param {DTS} settings
|
||||
* @returns {Date} date object
|
||||
*/
|
||||
function parseDate(value, settings) {
|
||||
/** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */
|
||||
var formatObj = {yyyy:null, yy:null, mm:null, dd:null};
|
||||
var format = ((settings.dateFormat) || '').toLowerCase();
|
||||
if (!format) {
|
||||
throw new TypeError('dateFormat not found (' + settings.dateFormat + ')');
|
||||
}
|
||||
var formatObj = parseData(value, format, formatObj, function(hooks){
|
||||
hooks.canSkip.add("yy", function(data){
|
||||
return data["yyyy"].value;
|
||||
});
|
||||
hooks.updateValue.add("yy", function(val){
|
||||
return 100 * Math.floor(new Date().getFullYear() / 100) + val;
|
||||
});
|
||||
});
|
||||
var year = formatObj["yyyy"].value || formatObj["yy"].value;
|
||||
var month = formatObj["mm"].value - 1;
|
||||
var date = formatObj["dd"].value;
|
||||
var result = new Date(year, month, date);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} value
|
||||
* @param {DTS} settings
|
||||
* @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1
|
||||
*/
|
||||
function parseTime(value, settings) {
|
||||
var format = ((settings.timeFormat) || '').toLowerCase();
|
||||
if (!format) {
|
||||
throw new TypeError('timeFormat not found (' + settings.timeFormat + ')');
|
||||
}
|
||||
|
||||
/** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */
|
||||
var formatObj = {hh:null, mm:null, ss:null, a:null};
|
||||
var formatObj = parseData(value, format, formatObj, function(hooks){
|
||||
hooks.updateValue.add("a", function(val, data, start, stop){
|
||||
return value.slice(start, start + 2);
|
||||
});
|
||||
});
|
||||
var hours = formatObj["hh"].value;
|
||||
var minutes = formatObj["mm"].value;
|
||||
var seconds = formatObj["ss"].value;
|
||||
var am_pm = formatObj["a"].value;
|
||||
var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm;
|
||||
if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){
|
||||
if (am_pm_lower == 'am' && hours == 12){
|
||||
hours = 0;
|
||||
} else if (am_pm_lower == 'pm') {
|
||||
hours += 12;
|
||||
}
|
||||
}
|
||||
var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} value
|
||||
* @param {DTS} settings
|
||||
* @returns {String} date string
|
||||
*/
|
||||
function renderDate(value, settings) {
|
||||
var format = settings.dateFormat.toLowerCase();
|
||||
var date = value.getDate();
|
||||
var month = value.getMonth() + 1;
|
||||
var year = value.getFullYear();
|
||||
var yearShort = year % 100;
|
||||
var formatObj = {
|
||||
dd: date < 10 ? "0" + date : date,
|
||||
mm: month < 10 ? "0" + month : month,
|
||||
yyyy: year,
|
||||
yy: yearShort < 10 ? "0" + yearShort : yearShort
|
||||
};
|
||||
var str = format.replace(settings.dateFormatRegEx, function (found) {
|
||||
return formatObj[found];
|
||||
});
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} value
|
||||
* @param {DTS} settings
|
||||
* @returns {String} date string
|
||||
*/
|
||||
function renderTime(value, settings) {
|
||||
var Format = settings.timeFormat;
|
||||
var format = Format.toLowerCase();
|
||||
var hours = value.getHours();
|
||||
var minutes = value.getMinutes();
|
||||
var seconds = value.getSeconds();
|
||||
var am_pm = null;
|
||||
var hh_am_pm = null;
|
||||
if (format.indexOf('a') > -1) {
|
||||
am_pm = hours >= 12 ? 'pm' : 'am';
|
||||
am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm;
|
||||
hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours);
|
||||
}
|
||||
var formatObj = {
|
||||
hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours),
|
||||
mm: minutes < 10 ? "0" + minutes : minutes,
|
||||
ss: seconds < 10 ? "0" + seconds : seconds,
|
||||
a: am_pm,
|
||||
};
|
||||
var str = format.replace(settings.timeFormatRegEx, function (found) {
|
||||
return formatObj[found];
|
||||
});
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if two dates are equal
|
||||
* @param {Date} date1
|
||||
* @param {Date} date2
|
||||
* @returns {Boolean} true or false
|
||||
*/
|
||||
function isEqualDate(date1, date2) {
|
||||
if (!(date1 && date2)) return false;
|
||||
return (date1.getFullYear() == date2.getFullYear() &&
|
||||
date1.getMonth() == date2.getMonth() &&
|
||||
date1.getDate() == date2.getDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} val
|
||||
* @param {Number} pad
|
||||
* @param {*} default_val
|
||||
* @returns {String} padded string
|
||||
*/
|
||||
function padded(val, pad, default_val) {
|
||||
var default_val = default_val || 0;
|
||||
var valStr = '' + (parseInt(val) || default_val);
|
||||
var diff = Math.max(pad, valStr.length) - valStr.length;
|
||||
return ('' + default_val).repeat(diff) + valStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template X
|
||||
* @template Y
|
||||
* @param {X} obj
|
||||
* @param {Y} objDefaults
|
||||
* @returns {X|Y} merged object
|
||||
*/
|
||||
function setDefaults(obj, objDefaults) {
|
||||
var keys = Object.keys(objDefaults);
|
||||
for (var i=0; i<keys.length; i++) {
|
||||
var key = keys[i];
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
obj[key] = objDefaults[key];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
window.dtsel = Object.create({},{
|
||||
DTS: { value: DTS },
|
||||
DTObj: { value: DTBox },
|
||||
fn: {
|
||||
value: Object.defineProperties({}, {
|
||||
empty: { value: empty },
|
||||
appendAfter: {
|
||||
value: function (newElem, refNode) {
|
||||
refNode.parentNode.insertBefore(newElem, refNode.nextSibling);
|
||||
},
|
||||
},
|
||||
getOffset: { value: getOffset },
|
||||
parseDate: { value: parseDate },
|
||||
renderDate: { value: renderDate },
|
||||
parseTime: {value: parseTime},
|
||||
renderTime: {value: renderTime},
|
||||
setDefaults: {value: setDefaults},
|
||||
}),
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
const common = require('./common')
|
||||
|
||||
exports.getArticle = function(id) {
|
||||
return common.sendRequest({
|
||||
method: 'GET',
|
||||
url: '/api/articles/' + id,
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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'),
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
*/
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|