Version 0.1
This commit is contained in:
parent
5d7eeefd79
commit
6d7b63eb47
58 changed files with 1276 additions and 303 deletions
44
.circleci/config.yml
Normal file
44
.circleci/config.yml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
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
|
||||||
|
command: apk update && apk upgrade && apk add --no-cache bash git openssh
|
||||||
|
- checkout
|
||||||
|
- setup_remote_docker
|
||||||
|
- 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}
|
||||||
|
- 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@82.221.107.21 "docker ${service_name} ${di}:${CIRCLE_SHA1} ${target_port} ${service_port}"
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build_deploy:
|
||||||
|
jobs:
|
||||||
|
- build:
|
||||||
|
context: org-global
|
10
.eslintrc
10
.eslintrc
|
@ -6,11 +6,15 @@
|
||||||
"impliedStrict": true
|
"impliedStrict": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"globals": {
|
||||||
|
"FroalaEditor": "readonly"
|
||||||
|
},
|
||||||
"extends": "eslint:recommended",
|
"extends": "eslint:recommended",
|
||||||
"env": {
|
"env": {
|
||||||
"mocha": true,
|
"mocha": true,
|
||||||
"node": true,
|
"node": true,
|
||||||
"es6": true
|
"es6": true,
|
||||||
|
"browser": true
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"require-await": 0,
|
"require-await": 0,
|
||||||
|
@ -48,7 +52,7 @@
|
||||||
"no-sync": 2,
|
"no-sync": 2,
|
||||||
"array-bracket-newline": [2, "consistent"],
|
"array-bracket-newline": [2, "consistent"],
|
||||||
"block-spacing": [2, "always"],
|
"block-spacing": [2, "always"],
|
||||||
"brace-style": [2, "1tbs"],
|
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
|
||||||
"comma-dangle": [2, "always-multiline"],
|
"comma-dangle": [2, "always-multiline"],
|
||||||
"comma-spacing": 2,
|
"comma-spacing": 2,
|
||||||
"comma-style": 2,
|
"comma-style": 2,
|
||||||
|
@ -66,7 +70,7 @@
|
||||||
2,
|
2,
|
||||||
{
|
{
|
||||||
"args": "after-used",
|
"args": "after-used",
|
||||||
"argsIgnorePattern": "next|res|req"
|
"argsIgnorePattern": "next|res|req|vnode"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generator-star-spacing": 0,
|
"generator-star-spacing": 0,
|
||||||
|
|
52
Dockerfile
Normal file
52
Dockerfile
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
###########################
|
||||||
|
# Angular
|
||||||
|
###########################
|
||||||
|
FROM node:alpine as build
|
||||||
|
|
||||||
|
ENV HOME=/app
|
||||||
|
|
||||||
|
COPY package.json $HOME/
|
||||||
|
COPY app $HOME/app
|
||||||
|
COPY public $HOME/public
|
||||||
|
|
||||||
|
WORKDIR $HOME
|
||||||
|
|
||||||
|
RUN apk add --update --no-cache --virtual .build-deps gcc g++ make libc6-compat python && \
|
||||||
|
apk add build-base --no-cache \
|
||||||
|
--repository https://alpine.global.ssl.fastly.net/alpine/edge/main \
|
||||||
|
--repository https://alpine.global.ssl.fastly.net/alpine/edge/testing \
|
||||||
|
--repository https://alpine.global.ssl.fastly.net/alpine/edge/community && \
|
||||||
|
npm install && \
|
||||||
|
apk del .build-deps gcc g++ make libc6-compat python && \
|
||||||
|
apk del build-base && \
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
###########################
|
||||||
|
# Server
|
||||||
|
###########################
|
||||||
|
FROM node:alpine
|
||||||
|
|
||||||
|
ENV HOME=/app
|
||||||
|
|
||||||
|
COPY index.mjs package.json server.mjs $HOME/
|
||||||
|
|
||||||
|
WORKDIR $HOME
|
||||||
|
|
||||||
|
RUN apk add --update --no-cache --virtual .build-deps gcc g++ make libc6-compat python && \
|
||||||
|
apk add vips-dev fftw-dev build-base --no-cache \
|
||||||
|
--repository https://alpine.global.ssl.fastly.net/alpine/edge/main \
|
||||||
|
--repository https://alpine.global.ssl.fastly.net/alpine/edge/testing \
|
||||||
|
--repository https://alpine.global.ssl.fastly.net/alpine/edge/community && \
|
||||||
|
npm install --production && \
|
||||||
|
rm -Rf $HOME/.npm $HOME/.npm $HOME/.config && \
|
||||||
|
apk del .build-deps gcc g++ make libc6-compat python && \
|
||||||
|
apk del build-base
|
||||||
|
|
||||||
|
COPY api $HOME/api
|
||||||
|
COPY migrations $HOME/migrations
|
||||||
|
COPY config $HOME/config
|
||||||
|
COPY --from=build /app/public $HOME/public
|
||||||
|
|
||||||
|
EXPOSE 4030
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
|
@ -1,4 +1,4 @@
|
||||||
import orgAccess from './index'
|
import orgAccess from './index.mjs'
|
||||||
|
|
||||||
export function accessChecks(opts = { }) {
|
export function accessChecks(opts = { }) {
|
||||||
const access = opts.access || orgAccess
|
const access = opts.access || orgAccess
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import bookshelf from '../bookshelf.mjs'
|
import bookshelf from '../bookshelf.mjs'
|
||||||
import Media from '../media/model.mjs'
|
import Media from '../media/model.mjs'
|
||||||
|
import File from '../file/model.mjs'
|
||||||
import Staff from '../staff/model.mjs'
|
import Staff from '../staff/model.mjs'
|
||||||
import Page from '../page/model.mjs'
|
import Page from '../page/model.mjs'
|
||||||
|
|
||||||
|
@ -38,6 +39,13 @@ const Article = bookshelf.createModel({
|
||||||
staff() {
|
staff() {
|
||||||
return this.belongsTo(Staff, 'staff_id')
|
return this.belongsTo(Staff, 'staff_id')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
files() {
|
||||||
|
return this.hasManyFiltered(File, 'file', 'article_id')
|
||||||
|
.query(qb => {
|
||||||
|
qb.orderBy('id', 'asc')
|
||||||
|
})
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
getSingle(id, withRelated = [], require = true, ctx = null) {
|
getSingle(id, withRelated = [], require = true, ctx = null) {
|
||||||
return this.query(qb => {
|
return this.query(qb => {
|
||||||
|
@ -46,6 +54,28 @@ const Article = bookshelf.createModel({
|
||||||
})
|
})
|
||||||
.fetch({ require, withRelated, ctx })
|
.fetch({ require, withRelated, ctx })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getAllFromPage(ctx, pageId, withRelated = [], orderBy = 'id') {
|
||||||
|
return this.query(qb => {
|
||||||
|
this.baseQueryAll(ctx, qb, {}, orderBy)
|
||||||
|
qb.leftOuterJoin('pages', 'articles.parent_id', 'pages.id')
|
||||||
|
qb.where(subq => {
|
||||||
|
subq.where('pages.id', pageId)
|
||||||
|
.orWhere('pages.parent_id', pageId)
|
||||||
|
})
|
||||||
|
qb.select('articles.*')
|
||||||
|
})
|
||||||
|
.fetchPage({
|
||||||
|
pageSize: ctx.state.pagination.perPage,
|
||||||
|
page: ctx.state.pagination.page,
|
||||||
|
withRelated,
|
||||||
|
ctx: ctx,
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
ctx.state.pagination.total = result.pagination.rowCount
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default Article
|
export default Article
|
||||||
|
|
|
@ -13,12 +13,14 @@ export default class ArticleRoutes {
|
||||||
async getAllArticles(ctx) {
|
async getAllArticles(ctx) {
|
||||||
await this.security.ensureIncludes(ctx)
|
await this.security.ensureIncludes(ctx)
|
||||||
|
|
||||||
let filter = {}
|
ctx.body = await this.Article.getAll(ctx, { }, ctx.state.filter.includes)
|
||||||
if (ctx.query.tree && ctx.query.tree === 'true') {
|
}
|
||||||
filter.parent_id = null
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.body = await this.Article.getAll(ctx, filter, ctx.state.filter.includes)
|
/** GET: /api/pages/:pageId/articles */
|
||||||
|
async getAllPageArticles(ctx) {
|
||||||
|
await this.security.ensureIncludes(ctx)
|
||||||
|
|
||||||
|
ctx.body = await this.Article.getAllFromPage(ctx, ctx.params.pageId, ctx.state.filter.includes, ctx.query.sort || '-id')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET: /api/articles/:id */
|
/** GET: /api/articles/:id */
|
||||||
|
|
|
@ -15,7 +15,7 @@ const validFields = [
|
||||||
]
|
]
|
||||||
|
|
||||||
export async function ensureIncludes(ctx) {
|
export async function ensureIncludes(ctx) {
|
||||||
let out = filter(ctx.state.filter.includes, ['staff', 'media', 'parent', 'banner'])
|
let out = filter(ctx.state.filter.includes, ['staff', 'media', 'parent', 'banner', 'files'])
|
||||||
|
|
||||||
if (out.length > 0) {
|
if (out.length > 0) {
|
||||||
ctx.throw(422, `Includes had following invalid values: ${out.join(', ')}`)
|
ctx.throw(422, `Includes had following invalid values: ${out.join(', ')}`)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import google from 'googleapis'
|
|
||||||
import googleauth from 'google-auth-library'
|
import googleauth from 'google-auth-library'
|
||||||
import config from '../config'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
const oauth2Client = new googleauth.OAuth2Client(config.get('googleid'))
|
const oauth2Client = new googleauth.OAuth2Client(config.get('googleid'))
|
||||||
|
|
||||||
|
@ -11,4 +10,3 @@ const oauth2Client = new googleauth.OAuth2Client(config.get('googleid'))
|
||||||
export function getProfile(token) {
|
export function getProfile(token) {
|
||||||
return oauth2Client.getTokenInfo(token)
|
return oauth2Client.getTokenInfo(token)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Staff from '../staff/model'
|
import Staff from '../staff/model.mjs'
|
||||||
import Jwt from '../jwt'
|
import Jwt from '../jwt.mjs'
|
||||||
import * as google from './google'
|
import * as google from './google.mjs'
|
||||||
|
|
||||||
export default class AuthRoutes {
|
export default class AuthRoutes {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
|
|
|
@ -2,9 +2,9 @@ import _ from 'lodash'
|
||||||
import knex from 'knex'
|
import knex from 'knex'
|
||||||
import bookshelf from 'bookshelf'
|
import bookshelf from 'bookshelf'
|
||||||
|
|
||||||
import config from './config'
|
import config from './config.mjs'
|
||||||
import defaults from './defaults'
|
import defaults from './defaults.mjs'
|
||||||
import log from './log'
|
import log from './log.mjs'
|
||||||
|
|
||||||
let connections = [config.get('knex:connection')]
|
let connections = [config.get('knex:connection')]
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,15 @@ nconf.env({
|
||||||
'knex__connection__user',
|
'knex__connection__user',
|
||||||
'knex__connection__database',
|
'knex__connection__database',
|
||||||
'knex__connection__password',
|
'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',
|
'bunyan__name',
|
||||||
'frontend__url',
|
'frontend__url',
|
||||||
'jwt__secret',
|
'jwt__secret',
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import path from 'path'
|
import bookshelf from '../bookshelf.mjs'
|
||||||
import bookshelf from '../bookshelf'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
@ -8,19 +8,38 @@ File model:
|
||||||
filename,
|
filename,
|
||||||
filetype,
|
filetype,
|
||||||
size,
|
size,
|
||||||
|
path,
|
||||||
staff_id,
|
staff_id,
|
||||||
article_id,
|
article_id,
|
||||||
is_deleted,
|
is_deleted,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
*url,
|
||||||
|
*magnet,
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const File = bookshelf.createModel({
|
const File = bookshelf.createModel({
|
||||||
tableName: 'files',
|
tableName: 'files',
|
||||||
|
|
||||||
|
virtuals: {
|
||||||
|
url() {
|
||||||
|
return `${File.baseUrl}${this.get('path')}`
|
||||||
|
},
|
||||||
|
|
||||||
|
magnet() {
|
||||||
|
let meta = this.get('meta')
|
||||||
|
if (!meta.torrent) return ''
|
||||||
|
return 'magnet:?'
|
||||||
|
+ 'xl=' + this.get('size')
|
||||||
|
+ '&dn=' + encodeURIComponent(meta.torrent.name)
|
||||||
|
+ '&xt=urn:btih:' + meta.torrent.hash
|
||||||
|
+ meta.torrent.announce.map(item => ('&tr=' + encodeURIComponent(item))).join('')
|
||||||
|
},
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
baseUrl: 'http://192.168.42.14',
|
baseUrl: config.get('upload:baseurl'),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default File
|
export default File
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import config from '../config'
|
import { readFile } from 'fs'
|
||||||
import File from './model'
|
import parseTorrent from 'parse-torrent'
|
||||||
import * as multer from '../multer'
|
|
||||||
import { uploadFile } from '../media/upload'
|
import config from '../config.mjs'
|
||||||
import Jwt from '../jwt'
|
import File from './model.mjs'
|
||||||
|
import * as multer from '../multer.mjs'
|
||||||
|
import { uploadFile } from '../media/upload.mjs'
|
||||||
|
import Jwt from '../jwt.mjs'
|
||||||
|
|
||||||
export default class FileRoutes {
|
export default class FileRoutes {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
|
@ -18,16 +21,35 @@ export default class FileRoutes {
|
||||||
let result = await this.multer.processBody(ctx)
|
let result = await this.multer.processBody(ctx)
|
||||||
|
|
||||||
let token = this.jwt.signDirect({ site: config.get('upload:name') }, config.get('upload:secret'))
|
let token = this.jwt.signDirect({ site: config.get('upload:name') }, config.get('upload:secret'))
|
||||||
|
let meta = {}
|
||||||
|
|
||||||
return ctx.throw(422, 'Unable to process for now')
|
if (result.originalname.endsWith('.torrent')) {
|
||||||
|
let fileContent = await new Promise((res, rej) => {
|
||||||
|
readFile(result.path, (err, data) => {
|
||||||
|
if (err) return rej(err)
|
||||||
|
res(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
let torrent = parseTorrent(fileContent)
|
||||||
|
meta = {
|
||||||
|
torrent: {
|
||||||
|
name: torrent.name,
|
||||||
|
announce: torrent.announce,
|
||||||
|
hash: torrent.infoHash,
|
||||||
|
files: torrent.files.map(file => ({ name: file.name, size: file.length })),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let file = await this.uploadFile(token, result.path)
|
let file = await this.uploadFile(token, result.path)
|
||||||
ctx.body = await this.File.create({
|
ctx.body = await this.File.create({
|
||||||
filename: result.originalname,
|
filename: result.originalname,
|
||||||
filetype: result.mimetype,
|
filetype: result.mimetype,
|
||||||
|
path: file.path,
|
||||||
article_id: ctx.params.articleId,
|
article_id: ctx.params.articleId,
|
||||||
size: result.size,
|
size: result.size,
|
||||||
staff_id: ctx.state.user.id,
|
staff_id: ctx.state.user.id,
|
||||||
|
meta: meta,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import dot from 'dot'
|
|
||||||
import fs from 'fs'
|
|
||||||
|
|
||||||
export default class IndexRoutes {
|
export default class IndexRoutes {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
this.indexBody = ''
|
this.indexBody = ''
|
||||||
})
|
}
|
||||||
|
|
||||||
async sendIndex(ctx) {
|
async sendIndex(ctx) {
|
||||||
ctx.body = this.indexBody
|
ctx.body = this.indexBody
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import koaJwt from 'koa-jwt'
|
import koaJwt from 'koa-jwt'
|
||||||
import Staff from './staff/model'
|
import Staff from './staff/model.mjs'
|
||||||
import config from './config'
|
import config from './config.mjs'
|
||||||
|
|
||||||
export default class Jwt {
|
export default class Jwt {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import bunyan from 'bunyan-lite'
|
import bunyan from 'bunyan-lite'
|
||||||
import config from './config'
|
import config from './config.mjs'
|
||||||
import * as defaults from './defaults'
|
import * as defaults from './defaults.mjs'
|
||||||
|
|
||||||
// Clone the settings as we will be touching
|
// Clone the settings as we will be touching
|
||||||
// on them slightly.
|
// on them slightly.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import bookshelf from '../bookshelf'
|
import bookshelf from '../bookshelf.mjs'
|
||||||
|
import config from '../config.mjs'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ const Media = bookshelf.createModel({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
baseUrl: 'http://192.168.42.14',
|
baseUrl: config.get('upload:baseurl'),
|
||||||
|
|
||||||
getSubUrl(input, size) {
|
getSubUrl(input, size) {
|
||||||
if (!input) return input
|
if (!input) return input
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
import Media from './model'
|
import Media from './model.mjs'
|
||||||
|
|
||||||
export default class Resizer {
|
export default class Resizer {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
|
@ -15,7 +15,7 @@ export default class Resizer {
|
||||||
return this.sharp(input)
|
return this.sharp(input)
|
||||||
.resize(360, 360, {
|
.resize(360, 360, {
|
||||||
fit: sharp.fit.inside,
|
fit: sharp.fit.inside,
|
||||||
withoutEnlargement: true
|
withoutEnlargement: true,
|
||||||
})
|
})
|
||||||
.toFile(output)
|
.toFile(output)
|
||||||
.then(() => output)
|
.then(() => output)
|
||||||
|
@ -27,7 +27,7 @@ export default class Resizer {
|
||||||
return this.sharp(input)
|
return this.sharp(input)
|
||||||
.resize(700, 700, {
|
.resize(700, 700, {
|
||||||
fit: sharp.fit.inside,
|
fit: sharp.fit.inside,
|
||||||
withoutEnlargement: true
|
withoutEnlargement: true,
|
||||||
})
|
})
|
||||||
.toFile(output)
|
.toFile(output)
|
||||||
.then(() => output)
|
.then(() => output)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import config from '../config'
|
import config from '../config.mjs'
|
||||||
import Media from './model'
|
import Media from './model.mjs'
|
||||||
import * as multer from '../multer'
|
import * as multer from '../multer.mjs'
|
||||||
import Resizer from './resize'
|
import Resizer from './resize.mjs'
|
||||||
import { uploadFile } from './upload'
|
import { uploadFile } from './upload.mjs'
|
||||||
import Jwt from '../jwt'
|
import Jwt from '../jwt.mjs'
|
||||||
|
|
||||||
export default class MediaRoutes {
|
export default class MediaRoutes {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import http from 'http'
|
import http from 'http'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import config from '../config'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
let stub
|
let stub
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import format from 'format-link-header'
|
import format from 'format-link-header'
|
||||||
|
|
||||||
import * as pagination from './pagination'
|
import * as pagination from './pagination.mjs'
|
||||||
|
|
||||||
export default class ParserMiddleware {
|
export default class ParserMiddleware {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { format } from 'url'
|
import { format } from 'url'
|
||||||
import config from '../config'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
function limit(value, min, max, fallback) {
|
function limit(value, min, max, fallback) {
|
||||||
let out = parseInt(value, 10)
|
let out = parseInt(value, 10)
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
/* eslint max-len: 0 */
|
/* eslint max-len: 0 */
|
||||||
import Router from 'koa-router'
|
import Router from 'koa-router'
|
||||||
|
|
||||||
import access from './access'
|
import access from './access/index.mjs'
|
||||||
import AuthRoutes from './authentication/routes'
|
import AuthRoutes from './authentication/routes.mjs'
|
||||||
import MediaRoutes from './media/routes'
|
import MediaRoutes from './media/routes.mjs'
|
||||||
import FileRoutes from './file/routes'
|
import FileRoutes from './file/routes.mjs'
|
||||||
import PageRoutes from './page/routes'
|
import PageRoutes from './page/routes.mjs'
|
||||||
import ArticleRoutes from './article/routes'
|
import ArticleRoutes from './article/routes.mjs'
|
||||||
import { restrict } from './access/middleware'
|
import { restrict } from './access/middleware.mjs'
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ router.del('/api/pages/:id', restrict(access.Manager), page.removePage.bind(page
|
||||||
|
|
||||||
const article = new ArticleRoutes()
|
const article = new ArticleRoutes()
|
||||||
router.get('/api/articles', article.getAllArticles.bind(article))
|
router.get('/api/articles', article.getAllArticles.bind(article))
|
||||||
|
router.get('/api/pages/:pageId/articles', article.getAllPageArticles.bind(article))
|
||||||
router.get('/api/articles/:id', article.getSingleArticle.bind(article))
|
router.get('/api/articles/:id', article.getSingleArticle.bind(article))
|
||||||
router.post('/api/articles', restrict(access.Manager), article.createArticle.bind(article))
|
router.post('/api/articles', restrict(access.Manager), article.createArticle.bind(article))
|
||||||
router.put('/api/articles/:id', restrict(access.Manager), article.updateArticle.bind(article))
|
router.put('/api/articles/:id', restrict(access.Manager), article.updateArticle.bind(article))
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
import config from './config'
|
import config from './config.mjs'
|
||||||
import log from './log'
|
import log from './log.mjs'
|
||||||
import knex from 'knex'
|
import knex from 'knex'
|
||||||
|
|
||||||
// This is important for setup to run cleanly.
|
// This is important for setup to run cleanly.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import bookshelf from '../bookshelf'
|
import bookshelf from '../bookshelf.mjs'
|
||||||
|
|
||||||
/* Staff model:
|
/* Staff model:
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,19 +1,39 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
|
|
||||||
const Authentication = require('../authentication')
|
const { getAllArticlesPagination, removeArticle } = require('../api/article')
|
||||||
const { getAllArticles, removeArticle } = require('../api/article')
|
const { fetchPage } = require('../api/pagination')
|
||||||
const Dialogue = require('../widgets/dialogue')
|
const Dialogue = require('../widgets/dialogue')
|
||||||
|
const Pages = require('../widgets/pages')
|
||||||
|
|
||||||
const AdminArticles = {
|
const AdminArticles = {
|
||||||
oninit: function(vnode) {
|
oninit: function(vnode) {
|
||||||
this.loading = true
|
|
||||||
this.error = ''
|
this.error = ''
|
||||||
|
this.lastpage = m.route.param('page') || '1'
|
||||||
this.articles = []
|
this.articles = []
|
||||||
this.removeArticle = null
|
this.removeArticle = null
|
||||||
|
|
||||||
getAllArticles()
|
this.fetchArticles(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
onupdate: function(vnode) {
|
||||||
|
if (m.route.param('page') && m.route.param('page') !== this.lastpage) {
|
||||||
|
this.fetchArticles(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchArticles: function(vnode) {
|
||||||
|
this.loading = true
|
||||||
|
this.links = null
|
||||||
|
this.lastpage = m.route.param('page') || '1'
|
||||||
|
|
||||||
|
return fetchPage(getAllArticlesPagination({
|
||||||
|
per_page: 10,
|
||||||
|
page: this.lastpage,
|
||||||
|
includes: ['parent'],
|
||||||
|
}))
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
vnode.state.articles = result
|
vnode.state.articles = result.data
|
||||||
|
vnode.state.links = result.links
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
vnode.state.error = err.message
|
vnode.state.error = err.message
|
||||||
|
@ -57,26 +77,26 @@ const AdminArticles = {
|
||||||
m('td', m(m.route.Link, { href: '/article/' + article.path }, '/article/' + article.path)),
|
m('td', m(m.route.Link, { href: '/article/' + article.path }, '/article/' + article.path)),
|
||||||
m('td.right', article.updated_at.replace('T', ' ').split('.')[0]),
|
m('td.right', article.updated_at.replace('T', ' ').split('.')[0]),
|
||||||
m('td.right', m('button', { onclick: function() { vnode.state.removeArticle = article } }, 'Remove')),
|
m('td.right', m('button', { onclick: function() { vnode.state.removeArticle = article } }, 'Remove')),
|
||||||
])
|
]),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
view: function(vnode) {
|
view: function(vnode) {
|
||||||
return [
|
return [
|
||||||
(this.loading ?
|
m('div.admin-wrapper', [
|
||||||
m('div.loading-spinner')
|
m('div.admin-actions', [
|
||||||
: m('div.admin-wrapper', [
|
m('span', 'Actions:'),
|
||||||
m('div.admin-actions', [
|
m(m.route.Link, { href: '/admin/articles/add' }, 'Create new article'),
|
||||||
m('span', 'Actions:'),
|
]),
|
||||||
m(m.route.Link, { href: '/admin/articles/add' }, 'Create new article'),
|
m('article.editarticle', [
|
||||||
]),
|
m('header', m('h1', 'All articles')),
|
||||||
m('article.editarticle', [
|
m('div.error', {
|
||||||
m('header', m('h1', 'All articles')),
|
hidden: !this.error,
|
||||||
m('div.error', {
|
onclick: function() { vnode.state.error = '' },
|
||||||
hidden: !this.error,
|
}, this.error),
|
||||||
onclick: function() { vnode.state.error = '' }
|
(this.loading
|
||||||
}, this.error),
|
? m('div.loading-spinner.full')
|
||||||
m('table', [
|
: m('table', [
|
||||||
m('thead',
|
m('thead',
|
||||||
m('tr', [
|
m('tr', [
|
||||||
m('th', 'Title'),
|
m('th', 'Title'),
|
||||||
|
@ -87,10 +107,14 @@ const AdminArticles = {
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
m('tbody', this.articles.map(AdminArticles.drawArticle.bind(this, vnode))),
|
m('tbody', this.articles.map(AdminArticles.drawArticle.bind(this, vnode))),
|
||||||
]),
|
])
|
||||||
]),
|
),
|
||||||
])
|
m(Pages, {
|
||||||
),
|
base: '/admin/articles',
|
||||||
|
links: this.links,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
m(Dialogue, {
|
m(Dialogue, {
|
||||||
hidden: vnode.state.removeArticle === null,
|
hidden: vnode.state.removeArticle === null,
|
||||||
title: 'Delete ' + (vnode.state.removeArticle ? vnode.state.removeArticle.name : ''),
|
title: 'Delete ' + (vnode.state.removeArticle ? vnode.state.removeArticle.name : ''),
|
||||||
|
|
|
@ -24,7 +24,7 @@ article.editarticle {
|
||||||
margin: 0 0 20px;
|
margin: 0 0 20px;
|
||||||
|
|
||||||
.inside {
|
.inside {
|
||||||
height: 100px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,11 +54,15 @@ article.editarticle {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&.full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileupload {
|
.fileupload {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
padding: 10.5em;
|
padding: 0.5em;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -80,6 +84,23 @@ article.editarticle {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
files {
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 40px 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 {
|
table span.subarticle {
|
||||||
|
|
|
@ -4,6 +4,8 @@ const Authentication = require('../authentication')
|
||||||
const FileUpload = require('../widgets/fileupload')
|
const FileUpload = require('../widgets/fileupload')
|
||||||
const Froala = require('./froala')
|
const Froala = require('./froala')
|
||||||
const { Tree } = require('../api/page')
|
const { Tree } = require('../api/page')
|
||||||
|
const { uploadFile } = require('../api/file')
|
||||||
|
const Fileinfo = require('../widgets/fileinfo')
|
||||||
const { createArticle, updateArticle, getArticle } = require('../api/article')
|
const { createArticle, updateArticle, getArticle } = require('../api/article')
|
||||||
|
|
||||||
const EditArticle = {
|
const EditArticle = {
|
||||||
|
@ -28,8 +30,30 @@ const EditArticle = {
|
||||||
},
|
},
|
||||||
|
|
||||||
oninit: function(vnode) {
|
oninit: function(vnode) {
|
||||||
this.loading = m.route.param('key') !== 'add'
|
this.froala = null
|
||||||
this.creating = m.route.param('key') === 'add'
|
this.loadedFroala = Froala.loadedFroala
|
||||||
|
|
||||||
|
if (!this.loadedFroala) {
|
||||||
|
Froala.createFroalaScript()
|
||||||
|
.then(function() {
|
||||||
|
vnode.state.loadedFroala = true
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchArticle(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
onupdate: function(vnode) {
|
||||||
|
if (this.lastid !== m.route.param('id')) {
|
||||||
|
this.fetchArticle(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchArticle: function(vnode) {
|
||||||
|
this.lastid = m.route.param('id')
|
||||||
|
this.loading = this.lastid !== 'add'
|
||||||
|
this.creating = this.lastid === 'add'
|
||||||
this.loadingFile = false
|
this.loadingFile = false
|
||||||
this.error = ''
|
this.error = ''
|
||||||
this.article = {
|
this.article = {
|
||||||
|
@ -38,13 +62,14 @@ const EditArticle = {
|
||||||
description: '',
|
description: '',
|
||||||
media: null,
|
media: null,
|
||||||
banner: null,
|
banner: null,
|
||||||
|
files: [],
|
||||||
}
|
}
|
||||||
this.editedPath = false
|
this.editedPath = false
|
||||||
this.froala = null
|
this.froala = null
|
||||||
this.loadedFroala = Froala.loadedFroala
|
this.loadedFroala = Froala.loadedFroala
|
||||||
|
|
||||||
if (m.route.param('key') !== 'add') {
|
if (this.lastid !== 'add') {
|
||||||
getArticle(m.route.param('key'))
|
getArticle(this.lastid)
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
vnode.state.editedPath = true
|
vnode.state.editedPath = true
|
||||||
vnode.state.article = result
|
vnode.state.article = result
|
||||||
|
@ -57,14 +82,6 @@ const EditArticle = {
|
||||||
m.redraw()
|
m.redraw()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.loadedFroala) {
|
|
||||||
Froala.createFroalaScript()
|
|
||||||
.then(function() {
|
|
||||||
vnode.state.loadedFroala = true
|
|
||||||
m.redraw()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateValue: function(name, e) {
|
updateValue: function(name, e) {
|
||||||
|
@ -83,16 +100,22 @@ const EditArticle = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fileUploaded: function(type, media) {
|
mediaUploaded: function(type, media) {
|
||||||
this.article[type] = media
|
this.article[type] = media
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mediaRemoved: function(type) {
|
||||||
|
this.article[type] = null
|
||||||
|
},
|
||||||
|
|
||||||
save: function(vnode, e) {
|
save: function(vnode, e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!this.article.name) {
|
if (!this.article.name) {
|
||||||
this.error = 'Name is missing'
|
this.error = 'Name is missing'
|
||||||
} else if (!this.article.path) {
|
} else if (!this.article.path) {
|
||||||
this.error = 'Path is missing'
|
this.error = 'Path is missing'
|
||||||
|
} else {
|
||||||
|
this.error = ''
|
||||||
}
|
}
|
||||||
if (this.error) return
|
if (this.error) return
|
||||||
|
|
||||||
|
@ -126,6 +149,7 @@ const EditArticle = {
|
||||||
if (vnode.state.article.id) {
|
if (vnode.state.article.id) {
|
||||||
res.media = vnode.state.article.media
|
res.media = vnode.state.article.media
|
||||||
res.banner = vnode.state.article.banner
|
res.banner = vnode.state.article.banner
|
||||||
|
res.files = vnode.state.article.files
|
||||||
vnode.state.article = res
|
vnode.state.article = res
|
||||||
} else {
|
} else {
|
||||||
m.route.set('/admin/articles/' + res.id)
|
m.route.set('/admin/articles/' + res.id)
|
||||||
|
@ -144,6 +168,19 @@ const EditArticle = {
|
||||||
if (!event.target.files[0]) return
|
if (!event.target.files[0]) return
|
||||||
vnode.state.error = ''
|
vnode.state.error = ''
|
||||||
vnode.state.loadingFile = true
|
vnode.state.loadingFile = true
|
||||||
|
|
||||||
|
uploadFile(this.article.id, event.target.files[0])
|
||||||
|
.then(function(res) {
|
||||||
|
vnode.state.article.files.push(res)
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
vnode.state.error = err.message
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
event.target.value = null
|
||||||
|
vnode.state.loadingFile = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getFlatTree: function() {
|
getFlatTree: function() {
|
||||||
|
@ -175,17 +212,19 @@ const EditArticle = {
|
||||||
m('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (this.article.name || '(untitled)'))),
|
m('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (this.article.name || '(untitled)'))),
|
||||||
m('div.error', {
|
m('div.error', {
|
||||||
hidden: !this.error,
|
hidden: !this.error,
|
||||||
onclick: function() { vnode.state.error = '' }
|
onclick: function() { vnode.state.error = '' },
|
||||||
}, this.error),
|
}, this.error),
|
||||||
m(FileUpload, {
|
m(FileUpload, {
|
||||||
onupload: this.fileUploaded.bind(this, 'banner'),
|
onupload: this.mediaUploaded.bind(this, 'banner'),
|
||||||
onerror: function(e) { vnode.state.error = e },
|
onerror: function(e) { vnode.state.error = e },
|
||||||
|
ondelete: this.mediaRemoved.bind(this, 'banner'),
|
||||||
media: this.article && this.article.banner,
|
media: this.article && this.article.banner,
|
||||||
}),
|
}),
|
||||||
m(FileUpload, {
|
m(FileUpload, {
|
||||||
class: 'cover',
|
class: 'cover',
|
||||||
useimg: true,
|
useimg: true,
|
||||||
onupload: this.fileUploaded.bind(this, 'media'),
|
onupload: this.mediaUploaded.bind(this, 'media'),
|
||||||
|
ondelete: this.mediaRemoved.bind(this, 'media'),
|
||||||
onerror: function(e) { vnode.state.error = e },
|
onerror: function(e) { vnode.state.error = e },
|
||||||
media: this.article && this.article.media,
|
media: this.article && this.article.media,
|
||||||
}),
|
}),
|
||||||
|
@ -226,15 +265,23 @@ const EditArticle = {
|
||||||
value: 'Save',
|
value: 'Save',
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
m('div.fileupload', [
|
this.article.files.length
|
||||||
'Add file',
|
? m('files', [
|
||||||
m('input', {
|
m('h4', 'Files'),
|
||||||
accept: '*',
|
this.article.files.map(function(item) { return m(Fileinfo, { file: item }) }),
|
||||||
type: 'file',
|
])
|
||||||
onchange: this.uploadFile.bind(this, vnode),
|
: null,
|
||||||
}),
|
this.article.id
|
||||||
(vnode.state.loading ? m('div.loading-spinner') : null),
|
? 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,
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
|
@ -84,6 +84,10 @@ const EditPage = {
|
||||||
this.page[type] = media
|
this.page[type] = media
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fileRemoved: function(type) {
|
||||||
|
this.page[type] = null
|
||||||
|
},
|
||||||
|
|
||||||
save: function(vnode, e) {
|
save: function(vnode, e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!this.page.name) {
|
if (!this.page.name) {
|
||||||
|
@ -105,8 +109,8 @@ const EditPage = {
|
||||||
path: this.page.path,
|
path: this.page.path,
|
||||||
parent_id: this.page.parent_id,
|
parent_id: this.page.parent_id,
|
||||||
description: this.page.description,
|
description: this.page.description,
|
||||||
banner_id: this.page.banner && this.page.banner.id,
|
banner_id: this.page.banner && this.page.banner.id || null,
|
||||||
media_id: this.page.media && this.page.media.id,
|
media_id: this.page.media && this.page.media.id || null,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
promise = createPage({
|
promise = createPage({
|
||||||
|
@ -114,8 +118,8 @@ const EditPage = {
|
||||||
path: this.page.path,
|
path: this.page.path,
|
||||||
parent_id: this.page.parent_id,
|
parent_id: this.page.parent_id,
|
||||||
description: this.page.description,
|
description: this.page.description,
|
||||||
banner_id: this.page.banner && this.page.banner.id,
|
banner_id: this.page.banner && this.page.banner.id || null,
|
||||||
media_id: this.page.media && this.page.media.id,
|
media_id: this.page.media && this.page.media.id || null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,6 +139,8 @@ const EditPage = {
|
||||||
vnode.state.loading = false
|
vnode.state.loading = false
|
||||||
m.redraw()
|
m.redraw()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
},
|
},
|
||||||
|
|
||||||
view: function(vnode) {
|
view: function(vnode) {
|
||||||
|
@ -147,16 +153,18 @@ const EditPage = {
|
||||||
? [
|
? [
|
||||||
m('span', 'Actions:'),
|
m('span', 'Actions:'),
|
||||||
m(m.route.Link, { href: '/page/' + this.page.path }, 'View page'),
|
m(m.route.Link, { href: '/page/' + this.page.path }, 'View page'),
|
||||||
|
m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'),
|
||||||
]
|
]
|
||||||
: null),
|
: null),
|
||||||
m('article.editpage', [
|
m('article.editpage', [
|
||||||
m('header', m('h1', this.creating ? 'Create Page' : 'Edit ' + (this.page.name || '(untitled)'))),
|
m('header', m('h1', this.creating ? 'Create Page' : 'Edit ' + (this.page.name || '(untitled)'))),
|
||||||
m('div.error', {
|
m('div.error', {
|
||||||
hidden: !this.error,
|
hidden: !this.error,
|
||||||
onclick: function() { vnode.state.error = '' }
|
onclick: function() { vnode.state.error = '' },
|
||||||
}, this.error),
|
}, this.error),
|
||||||
m(FileUpload, {
|
m(FileUpload, {
|
||||||
onupload: this.fileUploaded.bind(this, 'banner'),
|
onupload: this.fileUploaded.bind(this, 'banner'),
|
||||||
|
ondelete: this.fileRemoved.bind(this, 'banner'),
|
||||||
onerror: function(e) { vnode.state.error = e },
|
onerror: function(e) { vnode.state.error = e },
|
||||||
media: this.page && this.page.banner,
|
media: this.page && this.page.banner,
|
||||||
}),
|
}),
|
||||||
|
@ -164,6 +172,7 @@ const EditPage = {
|
||||||
class: 'cover',
|
class: 'cover',
|
||||||
useimg: true,
|
useimg: true,
|
||||||
onupload: this.fileUploaded.bind(this, 'media'),
|
onupload: this.fileUploaded.bind(this, 'media'),
|
||||||
|
ondelete: this.fileRemoved.bind(this, 'media'),
|
||||||
onerror: function(e) { vnode.state.error = e },
|
onerror: function(e) { vnode.state.error = e },
|
||||||
media: this.page && this.page.media,
|
media: this.page && this.page.media,
|
||||||
}),
|
}),
|
||||||
|
@ -173,7 +182,9 @@ const EditPage = {
|
||||||
m('label', 'Parent'),
|
m('label', 'Parent'),
|
||||||
m('select', {
|
m('select', {
|
||||||
onchange: this.updateParent.bind(this),
|
onchange: this.updateParent.bind(this),
|
||||||
}, parents.map(function(item) { return m('option', { value: item.id || -1, selected: item.id === vnode.state.page.parent_id }, item.name) })),
|
}, parents.map(function(item) {
|
||||||
|
return m('option', { value: item.id || -1, selected: item.id === vnode.state.page.parent_id }, item.name)
|
||||||
|
})),
|
||||||
m('label', 'Name'),
|
m('label', 'Name'),
|
||||||
m('input', {
|
m('input', {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
|
@ -63,7 +63,7 @@ const AdminPages = {
|
||||||
m('td', m(m.route.Link, { href: '/page/' + page.path }, '/page/' + page.path)),
|
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', page.updated_at.replace('T', ' ').split('.')[0]),
|
||||||
m('td.right', m('button', { onclick: function() { vnode.state.removePage = page } }, 'Remove')),
|
m('td.right', m('button', { onclick: function() { vnode.state.removePage = page } }, 'Remove')),
|
||||||
])
|
]),
|
||||||
].concat(page.children.map(AdminPages.drawPage.bind(this, vnode)))
|
].concat(page.children.map(AdminPages.drawPage.bind(this, vnode)))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ const AdminPages = {
|
||||||
m('header', m('h1', 'All pages')),
|
m('header', m('h1', 'All pages')),
|
||||||
m('div.error', {
|
m('div.error', {
|
||||||
hidden: !this.error,
|
hidden: !this.error,
|
||||||
onclick: function() { vnode.state.error = '' }
|
onclick: function() { vnode.state.error = '' },
|
||||||
}, this.error),
|
}, this.error),
|
||||||
m('table', [
|
m('table', [
|
||||||
m('thead',
|
m('thead',
|
||||||
|
|
|
@ -24,7 +24,7 @@ article.editpage {
|
||||||
margin: 0 0 20px;
|
margin: 0 0 20px;
|
||||||
|
|
||||||
.inside {
|
.inside {
|
||||||
height: 100px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,10 +23,55 @@ exports.getAllArticles = function() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/pages/' + pageId + '/articles?includes=' + includes.join(','),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getAllPageArticlesPagination = function(pageId, options) {
|
||||||
|
let extra = ''
|
||||||
|
|
||||||
|
if (options.sort) {
|
||||||
|
extra += '&sort=' + options.sort
|
||||||
|
}
|
||||||
|
if (options.per_page) {
|
||||||
|
extra += '&perPage=' + options.per_page
|
||||||
|
}
|
||||||
|
if (options.page) {
|
||||||
|
extra += '&page=' + options.page
|
||||||
|
}
|
||||||
|
if (options.includes) {
|
||||||
|
extra += '&includes=' + options.includes.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/api/pages/' + pageId + '/articles?' + extra
|
||||||
|
}
|
||||||
|
|
||||||
exports.getArticle = function(id) {
|
exports.getArticle = function(id) {
|
||||||
return sendRequest({
|
return sendRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/articles/' + id + '?includes=media,parent,banner',
|
url: '/api/articles/' + id + '?includes=media,parent,banner,files',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,38 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
const Authentication = require('../authentication')
|
const Authentication = require('../authentication')
|
||||||
|
|
||||||
exports.sendRequest = function(options) {
|
exports.sendRequest = function(options, isPagination) {
|
||||||
let token = Authentication.getToken()
|
let token = Authentication.getToken()
|
||||||
|
let pagination = isPagination
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
options.headers = options.headers || {}
|
options.headers = options.headers || {}
|
||||||
options.headers['Authorization'] = 'Bearer ' + token
|
options.headers['Authorization'] = 'Bearer ' + token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.extract = function(xhr) {
|
||||||
|
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 {
|
||||||
|
out = JSON.parse(xhr.responseText)
|
||||||
|
}
|
||||||
|
if (xhr.status >= 300) {
|
||||||
|
throw out
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
return m.request(options)
|
return m.request(options)
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
if (error.code === 403) {
|
if (error.code === 403) {
|
||||||
|
|
12
app/api/file.js
Normal file
12
app/api/file.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const { sendRequest } = require('./common')
|
||||||
|
|
||||||
|
exports.uploadFile = function(articleId, file) {
|
||||||
|
let formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
return sendRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/articles/' + articleId + '/file',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
}
|
16
app/api/pagination.js
Normal file
16
app/api/pagination.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
const parse = require('parse-link-header')
|
||||||
|
const { sendRequest } = require('./common')
|
||||||
|
|
||||||
|
exports.fetchPage = function(url) {
|
||||||
|
return sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: url,
|
||||||
|
}, true)
|
||||||
|
.then(result => {
|
||||||
|
return {
|
||||||
|
data: result.data,
|
||||||
|
links: parse(result.headers.link || ''),
|
||||||
|
total: Number(result.headers.pagination_total || '0'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
70
app/app.scss
70
app/app.scss
|
@ -17,7 +17,7 @@ body, h1, h2, h3, h4, h5, h6, p {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,9 +50,15 @@ img {
|
||||||
animation: spinner-loader .6s linear infinite;
|
animation: spinner-loader .6s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maincontainer {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
@ -186,37 +192,38 @@ table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
|
||||||
table thead th {
|
|
||||||
background-color: $headcolor;
|
|
||||||
border: solid 1px $bordercolor;
|
|
||||||
color: $headtext;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
table tbody td {
|
|
||||||
text-align: left;
|
|
||||||
border: solid 1px $bordercolor;
|
|
||||||
color: #333;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
table a,
|
|
||||||
table a:visited,
|
|
||||||
table a:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
color: $secondary-bg;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
table button {
|
thead th {
|
||||||
color: $secondary-bg;
|
background-color: $headcolor;
|
||||||
background: transparent;
|
border: solid 1px $bordercolor;
|
||||||
border: 1px solid $secondary-bg;
|
color: $headtext;
|
||||||
}
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
tbody td {
|
||||||
|
text-align: left;
|
||||||
|
border: solid 1px $bordercolor;
|
||||||
|
color: #333;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
a,
|
||||||
|
a:visited,
|
||||||
|
a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: $secondary-dark-bg;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
table td.right,
|
button {
|
||||||
table th.right {
|
color: $secondary-dark-bg;
|
||||||
text-align: right;
|
background: transparent;
|
||||||
|
border: 1px solid $secondary-dark-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.right,
|
||||||
|
th.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-container {
|
.floating-container {
|
||||||
|
@ -237,3 +244,4 @@ table th.right {
|
||||||
@import 'admin/admin';
|
@import 'admin/admin';
|
||||||
@import 'widgets/common';
|
@import 'widgets/common';
|
||||||
@import 'pages/page';
|
@import 'pages/page';
|
||||||
|
@import 'frontpage/frontpage'
|
||||||
|
|
|
@ -1,12 +1,78 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
|
|
||||||
|
const { getAllArticlesPagination } = require('../api/article')
|
||||||
|
const { fetchPage } = require('../api/pagination')
|
||||||
|
const Pages = require('../widgets/pages')
|
||||||
|
const Newsitem = require('../widgets/newsitem')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
view: function() {
|
oninit: function(vnode) {
|
||||||
return m('article', [
|
this.error = ''
|
||||||
m('header', [
|
this.loading = false
|
||||||
m('h1', 'Welcome to NFP Moe'),
|
this.featured = null
|
||||||
m('span.meta', 'Last updated many years ago'),
|
this.links = null
|
||||||
|
this.fetchArticles(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
onupdate: function(vnode) {
|
||||||
|
if (this.lastpage !== (m.route.param('page') || '1')) {
|
||||||
|
this.fetchArticles(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchArticles(vnode) {
|
||||||
|
this.error = ''
|
||||||
|
this.loading = true
|
||||||
|
this.links = null
|
||||||
|
this.articles = []
|
||||||
|
this.lastpage = m.route.param('page') || '1'
|
||||||
|
|
||||||
|
return fetchPage(getAllArticlesPagination({
|
||||||
|
per_page: 10,
|
||||||
|
page: this.lastpage,
|
||||||
|
includes: ['parent', 'files', 'media', 'banner'],
|
||||||
|
}))
|
||||||
|
.then(function(result) {
|
||||||
|
vnode.state.articles = result.data
|
||||||
|
vnode.state.links = result.links
|
||||||
|
|
||||||
|
for (var i = result.data.length - 1; i >= 0; i--) {
|
||||||
|
if (result.data[i].banner) {
|
||||||
|
vnode.state.featured = result.data[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
vnode.state.error = err.message
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
vnode.state.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return [
|
||||||
|
(this.featured && this.featured.banner
|
||||||
|
? m('a.frontpage-banner', {
|
||||||
|
href: '/article/' + this.featured.path,
|
||||||
|
style: { 'background-image': 'url("' + this.featured.banner.url + '")' },
|
||||||
|
},
|
||||||
|
this.featured.name
|
||||||
|
)
|
||||||
|
: null),
|
||||||
|
m('frontpage', [
|
||||||
|
(this.loading
|
||||||
|
? m('div.loading-spinner')
|
||||||
|
: null),
|
||||||
|
this.articles.map(function(article) {
|
||||||
|
return m(Newsitem, article)
|
||||||
|
}),
|
||||||
|
m(Pages, {
|
||||||
|
base: '/',
|
||||||
|
links: this.links,
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
])
|
]
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
32
app/frontpage/frontpage.scss
Normal file
32
app/frontpage/frontpage.scss
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
.frontpage-banner {
|
||||||
|
background-color: #999999;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
height: 150px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1920px;
|
||||||
|
align-self: center;
|
||||||
|
flex: 0 0 150px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 0 0.3em #000000;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 1.6em;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
frontpage {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: center;
|
||||||
|
margin: 0 20px;
|
||||||
|
padding: 0 20px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
max-width: 1000px;
|
||||||
|
flex: 2 0 0;
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ const m = require('mithril')
|
||||||
|
|
||||||
m.route.prefix = ''
|
m.route.prefix = ''
|
||||||
|
|
||||||
const Authentication = require('./authentication')
|
|
||||||
const Menu = require('./menu/menu')
|
const Menu = require('./menu/menu')
|
||||||
const Frontpage = require('./frontpage/frontpage')
|
const Frontpage = require('./frontpage/frontpage')
|
||||||
const Login = require('./login/login')
|
const Login = require('./login/login')
|
||||||
|
@ -20,10 +19,10 @@ m.route(mainRoot, '/', {
|
||||||
'/': Frontpage,
|
'/': Frontpage,
|
||||||
'/login': Login,
|
'/login': Login,
|
||||||
'/logout': Logout,
|
'/logout': Logout,
|
||||||
'/page/:key': Page,
|
'/page/:id': Page,
|
||||||
'/admin/pages': AdminPages,
|
'/admin/pages': AdminPages,
|
||||||
'/admin/pages/:key': EditPage,
|
'/admin/pages/:key': EditPage,
|
||||||
'/admin/articles': AdminArticles,
|
'/admin/articles': AdminArticles,
|
||||||
'/admin/articles/:key': EditArticle,
|
'/admin/articles/:id': EditArticle,
|
||||||
})
|
})
|
||||||
m.mount(menuRoot, Menu)
|
m.mount(menuRoot, Menu)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
const Authentication = require('../authentication')
|
const Authentication = require('../authentication')
|
||||||
const { getAllPages, Tree, getTree } = require('../api/page')
|
const { Tree, getTree } = require('../api/page')
|
||||||
|
|
||||||
const Menu = {
|
const Menu = {
|
||||||
currentActive: 'home',
|
currentActive: 'home',
|
||||||
|
@ -36,20 +36,26 @@ const Menu = {
|
||||||
view: function() {
|
view: function() {
|
||||||
return [
|
return [
|
||||||
m('div.top', [
|
m('div.top', [
|
||||||
m('h2', 'NFP Moe'),
|
m(m.route.Link,
|
||||||
|
{ href: '/', class: 'logo' },
|
||||||
|
m('h2', 'NFP Moe')
|
||||||
|
),
|
||||||
m('aside', Authentication.currentUser ? [
|
m('aside', Authentication.currentUser ? [
|
||||||
m('p', 'Welcome ' + Authentication.currentUser.email),
|
m('p', [
|
||||||
|
'Welcome ' + Authentication.currentUser.email,
|
||||||
|
m(m.route.Link, { href: '/logout' }, 'Logout'),
|
||||||
|
]),
|
||||||
(Authentication.currentUser.level >= 100
|
(Authentication.currentUser.level >= 100
|
||||||
? [
|
? [
|
||||||
m(m.route.Link, { href: '/admin/pages' }, 'Pages'),
|
m(m.route.Link, { href: '/admin/pages' }, 'Pages'),
|
||||||
m(m.route.Link, { href: '/admin/articles' }, 'Articles'),
|
m(m.route.Link, { href: '/admin/articles' }, 'Articles'),
|
||||||
|
m(m.route.Link, { href: '/admin/articles/add' }, 'Create article'),
|
||||||
]
|
]
|
||||||
: null
|
: null
|
||||||
),
|
),
|
||||||
m(m.route.Link, { href: '/logout' }, 'Logout')
|
|
||||||
] : [
|
] : [
|
||||||
m(m.route.Link, { href: '/login' }, 'Login')
|
m(m.route.Link, { href: '/login' }, 'Login'),
|
||||||
])
|
]),
|
||||||
]),
|
]),
|
||||||
m('nav', [
|
m('nav', [
|
||||||
m(m.route.Link, {
|
m(m.route.Link, {
|
||||||
|
@ -62,7 +68,7 @@ const Menu = {
|
||||||
m(m.route.Link, {
|
m(m.route.Link, {
|
||||||
href: '/page/' + page.path,
|
href: '/page/' + page.path,
|
||||||
class: Menu.currentActive === ('/page/' + page.path) ? 'active' : '',
|
class: Menu.currentActive === ('/page/' + page.path) ? 'active' : '',
|
||||||
}, page.name)
|
}, page.name),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
return m(m.route.Link, {
|
return m(m.route.Link, {
|
||||||
|
@ -73,7 +79,7 @@ const Menu = {
|
||||||
]),
|
]),
|
||||||
Menu.error ? m('div.menuerror', Menu.error) : null,
|
Menu.error ? m('div.menuerror', Menu.error) : null,
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Menu
|
module.exports = Menu
|
||||||
|
|
|
@ -5,28 +5,40 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.top {
|
.top {
|
||||||
background: url('./img/logo.png') 25px center no-repeat $primary-dark-bg;
|
background: $primary-dark-bg;
|
||||||
color: $primary-dark-fg;
|
color: $primary-dark-fg;
|
||||||
padding: 0 10px 0 120px;
|
padding: 0 10px 0 0;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
a.logo {
|
||||||
|
background: url('./img/logo.png') 25px center no-repeat transparent;
|
||||||
|
padding-left: 120px;
|
||||||
|
display: flex;
|
||||||
|
color: $primary-dark-fg;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
flex-grow: 2;
|
|
||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
aside {
|
||||||
|
flex-grow: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: $meta-light-fg;
|
color: $meta-light-fg;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a, a:visited {
|
a, a:visited {
|
||||||
|
|
|
@ -1,12 +1,23 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
const { getPage } = require('../api/page')
|
const { getPage } = require('../api/page')
|
||||||
|
const { getAllPageArticlesPagination } = require('../api/article')
|
||||||
|
const { fetchPage } = require('../api/pagination')
|
||||||
const Authentication = require('../authentication')
|
const Authentication = require('../authentication')
|
||||||
const Newsentry = require('../widgets/newsentry')
|
const Newsentry = require('../widgets/newsentry')
|
||||||
|
const Pages = require('../widgets/pages')
|
||||||
|
|
||||||
const Page = {
|
const Page = {
|
||||||
oninit: function(vnode) {
|
oninit: function(vnode) {
|
||||||
this.path = m.route.param('key')
|
|
||||||
this.error = ''
|
this.error = ''
|
||||||
|
this.lastpage = m.route.param('page') || '1'
|
||||||
|
this.loadingnews = false
|
||||||
|
this.fetchPage(vnode)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchPage: function(vnode) {
|
||||||
|
this.path = m.route.param('id')
|
||||||
|
this.news = []
|
||||||
|
this.newslinks = null
|
||||||
this.page = {
|
this.page = {
|
||||||
id: 0,
|
id: 0,
|
||||||
name: '',
|
name: '',
|
||||||
|
@ -14,7 +25,6 @@ const Page = {
|
||||||
description: '',
|
description: '',
|
||||||
media: null,
|
media: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
getPage(this.path)
|
getPage(this.path)
|
||||||
|
@ -25,7 +35,37 @@ const Page = {
|
||||||
vnode.state.error = err.message
|
vnode.state.error = err.message
|
||||||
})
|
})
|
||||||
.then(function() {
|
.then(function() {
|
||||||
vnode.state.loading = false
|
return vnode.state.fetchArticles(vnode)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onupdate: function(vnode) {
|
||||||
|
if (this.path !== m.route.param('id')) {
|
||||||
|
this.fetchPage(vnode)
|
||||||
|
} else if (m.route.param('page') && m.route.param('page') !== this.lastpage) {
|
||||||
|
this.fetchArticles(vnode)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchArticles: function(vnode) {
|
||||||
|
this.loadingnews = true
|
||||||
|
this.newslinks = null
|
||||||
|
this.lastpage = m.route.param('page') || '1'
|
||||||
|
|
||||||
|
return fetchPage(getAllPageArticlesPagination(this.page.id, {
|
||||||
|
per_page: 10,
|
||||||
|
page: this.lastpage,
|
||||||
|
includes: ['files', 'media'],
|
||||||
|
}))
|
||||||
|
.then(function(result) {
|
||||||
|
vnode.state.news = result.data
|
||||||
|
vnode.state.newslinks = result.links
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
vnode.state.error = err.message
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
vnode.state.loading = vnode.state.loadingnews = false
|
||||||
m.redraw()
|
m.redraw()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -35,7 +75,7 @@ const Page = {
|
||||||
this.loading ?
|
this.loading ?
|
||||||
m('div.loading-spinner')
|
m('div.loading-spinner')
|
||||||
: m('article.page', [
|
: m('article.page', [
|
||||||
this.page.banner ? m('.div.page-banner', { style: { 'background-image': 'url(' + this.page.banner.url + ')' } } ) : null,
|
this.page.banner ? m('.div.page-banner', { style: { 'background-image': 'url("' + this.page.banner.url + '")' } } ) : null,
|
||||||
m('header', m('h1', this.page.name)),
|
m('header', m('h1', this.page.name)),
|
||||||
m('.container', {
|
m('.container', {
|
||||||
class: this.page.children.length ? 'multi' : '',
|
class: this.page.children.length ? 'multi' : '',
|
||||||
|
@ -44,7 +84,7 @@ const Page = {
|
||||||
? m('aside.sidebar', [
|
? m('aside.sidebar', [
|
||||||
m('h4', 'View ' + this.page.name + ':'),
|
m('h4', 'View ' + this.page.name + ':'),
|
||||||
this.page.children.map(function(page) {
|
this.page.children.map(function(page) {
|
||||||
return m(m.route.Link, { href: '/page/' + page.path, }, page.name)
|
return m(m.route.Link, { href: '/page/' + page.path }, page.name)
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
: null,
|
: null,
|
||||||
|
@ -52,27 +92,35 @@ const Page = {
|
||||||
? m('.fr-view', [
|
? m('.fr-view', [
|
||||||
this.page.media ? m('img.page-cover', { src: this.page.media.url } ) : null,
|
this.page.media ? m('img.page-cover', { src: this.page.media.url } ) : null,
|
||||||
m.trust(this.page.description),
|
m.trust(this.page.description),
|
||||||
this.page.news.length && this.page.description
|
this.news.length && this.page.description
|
||||||
? m('aside.news', [
|
? m('aside.news', [
|
||||||
m('h4', 'Latest updates under ' + this.page.name + ':'),
|
m('h4', 'Latest posts under ' + this.page.name + ':'),
|
||||||
this.page.news.map(function(article) {
|
this.loadingnews ? m('div.loading-spinner') : this.news.map(function(article) {
|
||||||
return m(Newsentry, article)
|
return m(Newsentry, article)
|
||||||
}),
|
}),
|
||||||
|
m(Pages, {
|
||||||
|
base: '/page/' + this.page.path,
|
||||||
|
links: this.newslinks,
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
: null
|
: null,
|
||||||
])
|
])
|
||||||
: null,
|
: this.news.length
|
||||||
|
? m('aside.news.single', [
|
||||||
|
this.page.media ? m('img.page-cover', { src: this.page.media.url } ) : null,
|
||||||
|
m('h4', 'Latest posts under ' + this.page.name + ':'),
|
||||||
|
this.loadingnews ? m('div.loading-spinner') : this.news.map(function(article) {
|
||||||
|
return m(Newsentry, article)
|
||||||
|
}),
|
||||||
|
m(Pages, {
|
||||||
|
base: '/page/' + this.page.path,
|
||||||
|
links: this.newslinks,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
: this.page.media
|
||||||
|
? m('img.page-cover.single', { src: this.page.media.url } )
|
||||||
|
: null,
|
||||||
]),
|
]),
|
||||||
this.page.news.length && !this.page.description
|
|
||||||
? m('aside.news', {
|
|
||||||
class: this.page.description ? '' : 'single'
|
|
||||||
}, [
|
|
||||||
m('h4', 'Latest updates under ' + this.page.name + ':'),
|
|
||||||
this.page.news.map(function(article) {
|
|
||||||
return m(Newsentry, article)
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
: null,
|
|
||||||
Authentication.currentUser
|
Authentication.currentUser
|
||||||
? m('div.admin-actions', [
|
? m('div.admin-actions', [
|
||||||
m('span', 'Admin controls:'),
|
m('span', 'Admin controls:'),
|
||||||
|
|
|
@ -17,18 +17,26 @@ article.page {
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-banner {
|
.page-banner {
|
||||||
background-size: auto 100%;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
height: 100px;
|
height: 150px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1920px;
|
max-width: 1920px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
flex: 0 0 100px;
|
flex: 0 0 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-cover {
|
.page-cover {
|
||||||
margin: 0 -10px 20px;
|
margin: 0 -10px 20px;
|
||||||
|
|
||||||
|
&.single {
|
||||||
|
margin: 0 20px 20px;
|
||||||
|
padding: 0 20px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
max-width: 800px;
|
||||||
|
flex: 2 0 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-actions {
|
.admin-actions {
|
||||||
|
@ -49,14 +57,14 @@ article.page {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
// a {
|
||||||
display: inline-block;
|
// display: inline-block;
|
||||||
padding-top: 5px;
|
// padding-top: 5px;
|
||||||
text-decoration: none;
|
// text-decoration: none;
|
||||||
color: $secondary-bg;
|
// color: $secondary-dark-bg;
|
||||||
font-size: 14px;
|
// font-size: 14px;
|
||||||
font-weight: bold;
|
// font-weight: bold;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
@ -88,6 +96,10 @@ article.page {
|
||||||
a {
|
a {
|
||||||
padding: 5px 5px 0px;
|
padding: 5px 5px 0px;
|
||||||
display: block;
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: $secondary-dark-bg;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +124,11 @@ aside.news {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
position: relative;
|
||||||
|
height: 133px;
|
||||||
|
}
|
||||||
|
|
||||||
newsentry {
|
newsentry {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
}
|
}
|
||||||
|
@ -119,16 +136,26 @@ aside.news {
|
||||||
&.single {
|
&.single {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
flex: 2 0 0;
|
flex: 2 0 0;
|
||||||
|
padding: 0 20px 10px;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
|
||||||
& > h4 {
|
& > h4 {
|
||||||
display: none;
|
padding: 0 5px 5px;
|
||||||
|
border-bottom: 1px solid $border;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-device-width: 639px){
|
@media screen and (max-width: 800px){
|
||||||
|
article.page aside.sidebar {
|
||||||
|
width: 200px;
|
||||||
|
flex: 0 0 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 639px){
|
||||||
article.page .container {
|
article.page .container {
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
}
|
}
|
||||||
|
@ -142,7 +169,7 @@ aside.news {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-device-width: 360px){
|
@media screen and (max-width: 360px){
|
||||||
article.page {
|
article.page {
|
||||||
.container {
|
.container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -16,30 +16,29 @@ fileupload {
|
||||||
.showbordericon {
|
.showbordericon {
|
||||||
border: 3px solid $title-fg;
|
border: 3px solid $title-fg;
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHaSURBVHhe7dfPScRAAIXxFSxAPHjUkrx61DaswCJsw5MdCDYkgs6DDIQlu5l/Sd5Ovh88WDJ7yPCdcgAAAAAAAEDvHofBwFPYzzD9xoaew37D/obpt55hAy9h4xjjKDrDik7FiCPKiuZixBFlBakx4oiyoNwYcURZQGmMOKI0VBsjjigNtIoRR5QKrWPEEaVASoyPiWdx5840omRIifEe9nD0bDyd6T9TZ3FESZAa4yrsfvTseDrTf4hSISeGzAURohTKjSEpQYQomUpiSGoQIUqi0hiSE0SIMqMmhuQGEaKcUBtDSoIIUY60iCGlQYQog1YxpCaI7D5KyxhSG0R2G+U67Cts6sJxOTGkRRBJiaJ31x26chv2HTZ14dwY0iqInIuid9a7d2kqSkkMaRlEpqJ0HSMaRymNIa2DyDjKLmJEuuhrWGkMWSKI6J30bruJ0cpSQVCIIGYIYoYgZghihiBmCGKGIGYIYoYgZghihiBmCGKGIGYIYoYgZghihiBmCGKGIGYIYoYgZghihiBmCGKGIGYIYoYgZghihiBmCGKGIGYIYoYgZggCAACAjd2FfXY+3fFinPtG6GUX9a1DEDMEMUMQMwQxcxP21vl0RwAAAAAAAGCnDod/1p4xx4l+w0cAAAAASUVORK5CYII=');
|
background-image: url('./img/upload.svg');
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 32px;
|
background-size: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.showicon {
|
.showicon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 5px;
|
||||||
left: 50%;
|
right: 5px;
|
||||||
margin-left: -16px;
|
width: 50px;
|
||||||
margin-top: -16px;
|
height: 50px;
|
||||||
width: 32px;
|
background-image: url('./img/upload.svg');
|
||||||
height: 32px;
|
|
||||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHaSURBVHhe7dfPScRAAIXxFSxAPHjUkrx61DaswCJsw5MdCDYkgs6DDIQlu5l/Sd5Ovh88WDJ7yPCdcgAAAAAAAEDvHofBwFPYzzD9xoaew37D/obpt55hAy9h4xjjKDrDik7FiCPKiuZixBFlBakx4oiyoNwYcURZQGmMOKI0VBsjjigNtIoRR5QKrWPEEaVASoyPiWdx5840omRIifEe9nD0bDyd6T9TZ3FESZAa4yrsfvTseDrTf4hSISeGzAURohTKjSEpQYQomUpiSGoQIUqi0hiSE0SIMqMmhuQGEaKcUBtDSoIIUY60iCGlQYQog1YxpCaI7D5KyxhSG0R2G+U67Cts6sJxOTGkRRBJiaJ31x26chv2HTZ14dwY0iqInIuid9a7d2kqSkkMaRlEpqJ0HSMaRymNIa2DyDjKLmJEuuhrWGkMWSKI6J30bruJ0cpSQVCIIGYIYoYgZghihiBmCGKGIGYIYoYgZghihiBmCGKGIGYIYoYgZghihiBmCGKGIGYIYoYgZghihiBmCGKGIGYIYoYgZghihiBmCGKGIGYIYoYgZggCAACAjd2FfXY+3fFinPtG6GUX9a1DEDMEMUMQMwQxcxP21vl0RwAAAAAAAGCnDod/1p4xx4l+w0cAAAAASUVORK5CYII=');
|
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 32px;
|
background-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: calc(100% - 80px);
|
width: 100%;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
min-height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.display {
|
.display {
|
||||||
|
@ -70,6 +69,23 @@ fileupload {
|
||||||
text-indent: -9999px;
|
text-indent: -9999px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
border: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 60px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background-image: url('./img/delete.svg');
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: transparent;
|
||||||
|
background-size: contain;
|
||||||
|
z-index: 3;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogue {
|
dialogue {
|
||||||
|
@ -79,41 +95,41 @@ dialogue {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
|
||||||
|
|
||||||
dialogue h2 {
|
h2 {
|
||||||
background: $secondary-dark-bg;
|
background: $secondary-dark-bg;
|
||||||
color: $secondary-dark-fg;
|
color: $secondary-dark-fg;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogue p {
|
p {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogue .buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogue button {
|
button {
|
||||||
border: 1px solid $secondary-dark-bg;
|
border: 1px solid $secondary-dark-bg;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: $secondary-dark-bg;
|
color: $secondary-dark-bg;
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogue button.alert {
|
button.alert {
|
||||||
border-color: red;
|
border-color: red;
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogue button.cancel {
|
button.cancel {
|
||||||
border-color: #999;
|
border-color: #999;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newsentry {
|
newsentry {
|
||||||
|
@ -121,24 +137,36 @@ newsentry {
|
||||||
color: $meta-fg;
|
color: $meta-fg;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
a {
|
.title {
|
||||||
&.cover {
|
display: flex;
|
||||||
flex-shrink: 0;
|
margin-bottom: 10px !important;
|
||||||
margin-right: 10px;
|
|
||||||
|
|
||||||
img {
|
a {
|
||||||
max-height: 70px;
|
text-decoration: none;
|
||||||
width: auto;
|
color: $secondary-dark-bg;
|
||||||
}
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.nobg {
|
a.cover {
|
||||||
height: 70px;
|
flex-shrink: 0;
|
||||||
width: 124px;
|
margin-right: 10px;
|
||||||
background: #ddd;
|
width: 124px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 70px;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.nobg {
|
||||||
|
height: 70px;
|
||||||
|
width: 124px;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
.entrycontent {
|
.entrycontent {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -146,9 +174,148 @@ newsentry {
|
||||||
padding: 0 5px 5px;
|
padding: 0 5px 5px;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin-bottom: 10px !important;
|
margin-bottom: 0 !important;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entrymeta {
|
||||||
|
font-size: 10px;
|
||||||
|
color: $meta-fg;
|
||||||
|
font-weight: bold;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileinfo {
|
||||||
|
padding: 0 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
&.slim {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filetitle {
|
||||||
|
display: block;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
.prefix {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $secondary-dark-bg;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
padding-right: 5px;
|
||||||
|
border-right: 1px solid $border;
|
||||||
|
margin-right: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 0;
|
||||||
|
list-style-type: disc;
|
||||||
|
list-style-position: inside;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newsitem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 15px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-decoration: none;
|
||||||
|
background: $secondary-bg;
|
||||||
|
color: $secondary-fg;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsitemcontent {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.cover {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 225px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.nobg {
|
||||||
|
height: 225px;
|
||||||
|
width: 400px;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entrycontent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 2 1 auto;
|
||||||
|
padding: 0 5px 5px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fr-view {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileinfo {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.entrymeta {
|
||||||
|
flex-grow: 2;
|
||||||
|
font-size: 11px;
|
||||||
|
color: $meta-fg;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pages {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
a, div {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9em;
|
||||||
|
max-width: 80px;
|
||||||
|
flex-grow: 2;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px !important;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $secondary-dark-bg;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
73
app/widgets/fileinfo.js
Normal file
73
app/widgets/fileinfo.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
|
||||||
|
const Fileinfo = {
|
||||||
|
getPrefix(vnode) {
|
||||||
|
if (!vnode.attrs.file.filename.endsWith('.torrent')) {
|
||||||
|
return vnode.attrs.file.filename.split('.').slice(-1)
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.indexOf('720 ') >= 0) {
|
||||||
|
return '720p'
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.indexOf('1080 ') >= 0) {
|
||||||
|
return '1080p'
|
||||||
|
}
|
||||||
|
if (vnode.attrs.file.filename.indexOf('480 ') >= 0) {
|
||||||
|
return '480p'
|
||||||
|
}
|
||||||
|
return 'Other'
|
||||||
|
},
|
||||||
|
|
||||||
|
getTitle(vnode) {
|
||||||
|
if (vnode.attrs.file.meta.torrent) {
|
||||||
|
return vnode.attrs.file.meta.torrent.name
|
||||||
|
}
|
||||||
|
return vnode.attrs.file.filename
|
||||||
|
},
|
||||||
|
|
||||||
|
getDownloadName(vnode) {
|
||||||
|
if (vnode.attrs.file.meta.torrent) {
|
||||||
|
return 'Torrent'
|
||||||
|
}
|
||||||
|
return 'Download'
|
||||||
|
},
|
||||||
|
|
||||||
|
getSize(orgSize) {
|
||||||
|
var size = orgSize
|
||||||
|
var i = -1
|
||||||
|
var byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
do {
|
||||||
|
size = size / 1024
|
||||||
|
i++
|
||||||
|
} while (size > 1024)
|
||||||
|
|
||||||
|
return Math.max(size, 0.1).toFixed(1) + byteUnits[i]
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return m('fileinfo', { class: vnode.attrs.slim ? 'slim' : ''}, [
|
||||||
|
m('div.filetitle', [
|
||||||
|
m('span.prefix', this.getPrefix(vnode) + ':'),
|
||||||
|
m('a', {
|
||||||
|
target: '_blank',
|
||||||
|
href: vnode.attrs.file.url,
|
||||||
|
}, this.getDownloadName(vnode)),
|
||||||
|
vnode.attrs.file.magnet
|
||||||
|
? m('a', {
|
||||||
|
href: vnode.attrs.file.magnet,
|
||||||
|
}, 'Magnet')
|
||||||
|
: null,
|
||||||
|
m('span', this.getTitle(vnode)),
|
||||||
|
]),
|
||||||
|
vnode.attrs.file.meta.torrent && !vnode.attrs.slim
|
||||||
|
? m('ul', vnode.attrs.file.meta.torrent.files.map(function(file) {
|
||||||
|
return m('li', [
|
||||||
|
file.name + ' ',
|
||||||
|
m('span.meta', '(' + Fileinfo.getSize(file.size) + ')'),
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
: null,
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Fileinfo
|
|
@ -17,6 +17,7 @@ const FileUpload = {
|
||||||
vnode.state.updateError(vnode, err.message)
|
vnode.state.updateError(vnode, err.message)
|
||||||
})
|
})
|
||||||
.then(function() {
|
.then(function() {
|
||||||
|
event.target.value = null
|
||||||
vnode.state.loading = false
|
vnode.state.loading = false
|
||||||
m.redraw()
|
m.redraw()
|
||||||
})
|
})
|
||||||
|
@ -50,7 +51,7 @@ const FileUpload = {
|
||||||
: m('a.display.inside', {
|
: m('a.display.inside', {
|
||||||
href: media.large_url,
|
href: media.large_url,
|
||||||
style: {
|
style: {
|
||||||
'background-image': 'url(' + media.medium_url + ')',
|
'background-image': 'url("' + media.large_url + '")',
|
||||||
},
|
},
|
||||||
}, m('div.showicon'))
|
}, m('div.showicon'))
|
||||||
: m('div.inside.showbordericon')
|
: m('div.inside.showbordericon')
|
||||||
|
@ -60,9 +61,10 @@ const FileUpload = {
|
||||||
type: 'file',
|
type: 'file',
|
||||||
onchange: this.uploadFile.bind(this, vnode),
|
onchange: this.uploadFile.bind(this, vnode),
|
||||||
}),
|
}),
|
||||||
|
(media && vnode.attrs.ondelete ? m('button.remove', { onclick: vnode.attrs.ondelete }) : null),
|
||||||
(vnode.state.loading ? m('div.loading-spinner') : null),
|
(vnode.state.loading ? m('div.loading-spinner') : null),
|
||||||
])
|
])
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FileUpload
|
module.exports = FileUpload
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
|
const Fileinfo = require('./fileinfo')
|
||||||
|
|
||||||
const Newsentry = {
|
const Newsentry = {
|
||||||
|
strip: function(html) {
|
||||||
|
var doc = new DOMParser().parseFromString(html, 'text/html')
|
||||||
|
var out = doc.body.textContent || ''
|
||||||
|
var splitted = out.split('.')
|
||||||
|
if (splitted.length > 2) {
|
||||||
|
return splitted.slice(0, 2).join('.') + '...'
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
|
|
||||||
view: function(vnode) {
|
view: function(vnode) {
|
||||||
return m('newsentry', [
|
return m('newsentry', [
|
||||||
vnode.attrs.media
|
vnode.attrs.media
|
||||||
|
@ -9,12 +20,21 @@ const Newsentry = {
|
||||||
}, m('img', { src: vnode.attrs.media.small_url }))
|
}, m('img', { src: vnode.attrs.media.small_url }))
|
||||||
: m('a.cover.nobg'),
|
: m('a.cover.nobg'),
|
||||||
m('div.entrycontent', [
|
m('div.entrycontent', [
|
||||||
m(m.route.Link,
|
m('div.title', [
|
||||||
{ href: '/article/' + vnode.attrs.path },
|
m(m.route.Link,
|
||||||
m('h3', vnode.attrs.name)
|
{ href: '/article/' + vnode.attrs.path },
|
||||||
),
|
m('h3', [vnode.attrs.name])
|
||||||
m('div.entrymeta', 'Posted ' + vnode.attrs.created_at.replace('T', ' ').split('.')[0])
|
),
|
||||||
])
|
]),
|
||||||
|
(vnode.attrs.files && vnode.attrs.files.length
|
||||||
|
? vnode.attrs.files.map(function(file) {
|
||||||
|
return m(Fileinfo, { file: file, slim: true })
|
||||||
|
})
|
||||||
|
: vnode.attrs.description
|
||||||
|
? m('span.entrydescription', Newsentry.strip(vnode.attrs.description))
|
||||||
|
: null),
|
||||||
|
m('span.entrymeta', 'Posted ' + vnode.attrs.created_at.replace('T', ' ').split('.')[0]),
|
||||||
|
]),
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
33
app/widgets/newsitem.js
Normal file
33
app/widgets/newsitem.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const Fileinfo = require('./fileinfo')
|
||||||
|
|
||||||
|
const Newsitem = {
|
||||||
|
view: function(vnode) {
|
||||||
|
return m('newsitem', [
|
||||||
|
m(m.route.Link,
|
||||||
|
{ href: '/article/' + vnode.attrs.path, class: 'title' },
|
||||||
|
m('h3', [vnode.attrs.name])
|
||||||
|
),
|
||||||
|
m('div.newsitemcontent', [
|
||||||
|
vnode.attrs.media
|
||||||
|
? m('a.cover', {
|
||||||
|
href: vnode.attrs.media.large_url,
|
||||||
|
}, m('img', { src: vnode.attrs.media.small_url }))
|
||||||
|
: m('a.cover.nobg'),
|
||||||
|
m('div.entrycontent', [
|
||||||
|
(vnode.attrs.description
|
||||||
|
? m('.fr-view', m.trust(vnode.attrs.description))
|
||||||
|
: null),
|
||||||
|
(vnode.attrs.files && vnode.attrs.files.length
|
||||||
|
? vnode.attrs.files.map(function(file) {
|
||||||
|
return m(Fileinfo, { file: file })
|
||||||
|
})
|
||||||
|
: null),
|
||||||
|
m('span.entrymeta', 'Posted ' + vnode.attrs.created_at.replace('T', ' ').split('.')[0]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Newsitem
|
40
app/widgets/pages.js
Normal file
40
app/widgets/pages.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
|
||||||
|
const Pages = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.onpage = vnode.attrs.onpage || function() {}
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
if (!vnode.attrs.links) return null
|
||||||
|
return m('pages', [
|
||||||
|
vnode.attrs.links.first
|
||||||
|
? m(m.route.Link, {
|
||||||
|
href: vnode.attrs.base + '?page=' + vnode.attrs.links.first.page,
|
||||||
|
onclick: function() { vnode.state.onpage(vnode.attrs.links.first.page) },
|
||||||
|
}, 'First')
|
||||||
|
: m('div'),
|
||||||
|
vnode.attrs.links.previous
|
||||||
|
? m(m.route.Link, {
|
||||||
|
href: vnode.attrs.base + '?page=' + vnode.attrs.links.previous.page,
|
||||||
|
onclick: function() { vnode.state.onpage(vnode.attrs.links.previous.page) },
|
||||||
|
}, vnode.attrs.links.previous.title)
|
||||||
|
: m('div'),
|
||||||
|
m('div', vnode.attrs.links.current && vnode.attrs.links.current.title || 'Current page'),
|
||||||
|
vnode.attrs.links.next
|
||||||
|
? m(m.route.Link, {
|
||||||
|
href: vnode.attrs.base + '?page=' + vnode.attrs.links.next.page,
|
||||||
|
onclick: function() { vnode.state.onpage(vnode.attrs.links.next.page) },
|
||||||
|
}, vnode.attrs.links.next.title)
|
||||||
|
: m('div'),
|
||||||
|
vnode.attrs.links.last
|
||||||
|
? m(m.route.Link, {
|
||||||
|
href: vnode.attrs.base + '?page=' + vnode.attrs.links.last.page,
|
||||||
|
onclick: function() { vnode.state.onpage(vnode.attrs.links.last.page) },
|
||||||
|
}, 'Last')
|
||||||
|
: m('div'),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Pages
|
|
@ -39,6 +39,7 @@
|
||||||
"bcrypt": 5,
|
"bcrypt": 5,
|
||||||
"fileSize": 524288000,
|
"fileSize": 524288000,
|
||||||
"upload": {
|
"upload": {
|
||||||
|
"baseurl": "http://192.168.42.14",
|
||||||
"port": "2111",
|
"port": "2111",
|
||||||
"host": "storage01.nfp.is",
|
"host": "storage01.nfp.is",
|
||||||
"name": "nfpmoe-dev",
|
"name": "nfpmoe-dev",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import log from './api/log'
|
import log from './api/log.mjs'
|
||||||
|
|
||||||
// Run the database script automatically.
|
// Run the database script automatically.
|
||||||
import setup from './api/setup'
|
import setup from './api/setup.mjs'
|
||||||
|
|
||||||
setup().catch(async (error) => {
|
setup().catch(async (error) => {
|
||||||
log.error({ code: error.code, message: error.message }, 'Error while preparing database')
|
log.error({ code: error.code, message: error.message }, 'Error while preparing database')
|
||||||
|
@ -13,7 +13,7 @@ setup().catch(async (error) => {
|
||||||
// process.exit(1)
|
// process.exit(1)
|
||||||
// })
|
// })
|
||||||
}).then(() =>
|
}).then(() =>
|
||||||
import('./server')
|
import('./server.mjs')
|
||||||
).catch(error => {
|
).catch(error => {
|
||||||
log.error(error, 'Unknown error starting server')
|
log.error(error, 'Unknown error starting server')
|
||||||
})
|
})
|
||||||
|
|
|
@ -36,18 +36,6 @@ exports.up = function up(knex, Promise) {
|
||||||
.default(false)
|
.default(false)
|
||||||
table.timestamps()
|
table.timestamps()
|
||||||
}),
|
}),
|
||||||
knex.schema.createTable('files', function(table) {
|
|
||||||
table.increments()
|
|
||||||
table.text('filename')
|
|
||||||
table.text('filetype')
|
|
||||||
table.integer('size')
|
|
||||||
table.integer('staff_id')
|
|
||||||
.references('staff.id')
|
|
||||||
table.boolean('is_deleted')
|
|
||||||
.notNullable()
|
|
||||||
.default(false)
|
|
||||||
table.timestamps()
|
|
||||||
}),
|
|
||||||
knex.schema.createTable('pages', function(table) {
|
knex.schema.createTable('pages', function(table) {
|
||||||
table.increments()
|
table.increments()
|
||||||
table.integer('staff_id')
|
table.integer('staff_id')
|
||||||
|
@ -90,13 +78,15 @@ exports.up = function up(knex, Promise) {
|
||||||
}),
|
}),
|
||||||
knex.schema.createTable('files', function(table) {
|
knex.schema.createTable('files', function(table) {
|
||||||
table.increments()
|
table.increments()
|
||||||
table.integer('file_id')
|
table.integer('articdle_id')
|
||||||
.references('files.id')
|
.references('articles.id')
|
||||||
table.text('filename')
|
table.text('filename')
|
||||||
table.text('filetype')
|
table.text('filetype')
|
||||||
|
table.text('path')
|
||||||
table.integer('size')
|
table.integer('size')
|
||||||
table.integer('staff_id')
|
table.integer('staff_id')
|
||||||
.references('staff.id')
|
.references('staff.id')
|
||||||
|
table.jsonb('meta')
|
||||||
table.boolean('is_deleted')
|
table.boolean('is_deleted')
|
||||||
.notNullable()
|
.notNullable()
|
||||||
.default(false)
|
.default(false)
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
{
|
{
|
||||||
"ignore": ["app/**", "public/**"]
|
"ignore": ["app/**", "public/**"],
|
||||||
|
"watch": [
|
||||||
|
"api/**",
|
||||||
|
"server*",
|
||||||
|
"config/**"
|
||||||
|
]
|
||||||
}
|
}
|
24
package.json
24
package.json
|
@ -6,17 +6,31 @@
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
|
"watch": {
|
||||||
|
"test": {
|
||||||
|
"patterns": [
|
||||||
|
"{api,test}/*"
|
||||||
|
],
|
||||||
|
"extensions": "js,mjs",
|
||||||
|
"quiet": false,
|
||||||
|
"inherit": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"start": "node --experimental-modules index.mjs",
|
"start": "node --experimental-modules index.mjs",
|
||||||
"build": "sass -s compressed app/app.scss public/assets/app.css && browserify -p tinyify --bare --no-browser-field -o public/assets/app.js app/index.js",
|
"build": "sass -s compressed app/app.scss public/assets/app.css && browserify -p tinyify --no-commondir -o public/assets/app.js app/index.js",
|
||||||
"build:check": "browserify -t uglifyify --bare --no-browser-field --list app/index.js",
|
"build:check": "browserify -o public/assets/app.js app/index.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"watch:api": "nodemon --experimental-modules index.mjs | bunyan",
|
"watch:api": "nodemon --experimental-modules index.mjs | bunyan",
|
||||||
"watch:app": "watchify -g envify -d app/index.js -o public/assets/app.js",
|
"watch:app": "watchify -g envify -d app/index.js -o public/assets/app.js",
|
||||||
"watch:sass": "sass --watch app/app.scss public/assets/app.css",
|
"watch:sass": "sass --watch app/app.scss public/assets/app.css",
|
||||||
"dev": "run-p watch:api watch:app watch:sass",
|
"dev": "run-p watch:api watch:app watch:sass",
|
||||||
"prod": "npm run build && npm start"
|
"prod": "npm run build && npm start",
|
||||||
|
"docker": "docker run -it --rm --name nfp_moe -e knex__connection__host -e NODE_ENV -p 4030:4030 -v \"$PWD\":/usr/src/app -w /usr/src/app node",
|
||||||
|
"docker:install": "npm run docker -- npm install",
|
||||||
|
"docker:dev": "npm run docker -- npm run dev",
|
||||||
|
"docker:prod": "npm run docker -- npm run prod"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -32,7 +46,6 @@
|
||||||
"@koa/cors": "^2.2.3",
|
"@koa/cors": "^2.2.3",
|
||||||
"bookshelf": "^0.15.1",
|
"bookshelf": "^0.15.1",
|
||||||
"bunyan-lite": "^1.0.1",
|
"bunyan-lite": "^1.0.1",
|
||||||
"dot": "^1.1.2",
|
|
||||||
"format-link-header": "^2.1.0",
|
"format-link-header": "^2.1.0",
|
||||||
"googleapis": "^42.0.0",
|
"googleapis": "^42.0.0",
|
||||||
"http-errors": "^1.7.2",
|
"http-errors": "^1.7.2",
|
||||||
|
@ -49,7 +62,7 @@
|
||||||
"nconf": "^0.10.0",
|
"nconf": "^0.10.0",
|
||||||
"parse-torrent": "^7.0.1",
|
"parse-torrent": "^7.0.1",
|
||||||
"pg": "^7.8.0",
|
"pg": "^7.8.0",
|
||||||
"sharp": "^0.21.3"
|
"sharp": "^0.22.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browserify": "^16.2.3",
|
"browserify": "^16.2.3",
|
||||||
|
@ -57,6 +70,7 @@
|
||||||
"mithril": "^2.0.3",
|
"mithril": "^2.0.3",
|
||||||
"nodemon": "^1.18.10",
|
"nodemon": "^1.18.10",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"parse-link-header": "^1.0.1",
|
||||||
"sass": "^1.17.0",
|
"sass": "^1.17.0",
|
||||||
"tinyify": "^2.5.1",
|
"tinyify": "^2.5.1",
|
||||||
"watchify": "^3.11.0"
|
"watchify": "^3.11.0"
|
||||||
|
|
23
public/assets/img/delete.svg
Normal file
23
public/assets/img/delete.svg
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?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>
|
After Width: | Height: | Size: 2.2 KiB |
28
public/assets/img/upload.svg
Normal file
28
public/assets/img/upload.svg
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<?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>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -10,7 +10,7 @@
|
||||||
<meta name="google-signin-client_id" content="1076074914074-3no1difo1jq3dfug3glfb25pn1t8idud.apps.googleusercontent.com">
|
<meta name="google-signin-client_id" content="1076074914074-3no1difo1jq3dfug3glfb25pn1t8idud.apps.googleusercontent.com">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="maincontainer">
|
||||||
<div id="nav"></div>
|
<div id="nav"></div>
|
||||||
<main id="main"></main>
|
<main id="main"></main>
|
||||||
</div>
|
</div>
|
||||||
|
|
18
server.mjs
18
server.mjs
|
@ -2,15 +2,15 @@ import Koa from 'koa'
|
||||||
import bodyParser from 'koa-bodyparser'
|
import bodyParser from 'koa-bodyparser'
|
||||||
import cors from '@koa/cors'
|
import cors from '@koa/cors'
|
||||||
|
|
||||||
import config from './api/config'
|
import config from './api/config.mjs'
|
||||||
import router from './api/router'
|
import router from './api/router.mjs'
|
||||||
import Jwt from './api/jwt'
|
import Jwt from './api/jwt.mjs'
|
||||||
import log from './api/log'
|
import log from './api/log.mjs'
|
||||||
import { serve } from './api/serve'
|
import { serve } from './api/serve.mjs'
|
||||||
import { mask } from './api/middlewares/mask'
|
import { mask } from './api/middlewares/mask.mjs'
|
||||||
import { errorHandler } from './api/error/middleware'
|
import { errorHandler } from './api/error/middleware.mjs'
|
||||||
import { accessChecks } from './api/access/middleware'
|
import { accessChecks } from './api/access/middleware.mjs'
|
||||||
import ParserMiddleware from './api/parser/middleware'
|
import ParserMiddleware from './api/parser/middleware.mjs'
|
||||||
|
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
const parser = new ParserMiddleware()
|
const parser = new ParserMiddleware()
|
||||||
|
|
Loading…
Reference in a new issue