diff --git a/.circleci/config.yml b/.circleci/config.yml index 03feabf..0ddb056 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,6 +16,9 @@ jobs: command: apk update && apk upgrade && apk add --no-cache bash git openssh - checkout - setup_remote_docker + - run: + name: Replace version in config + command: sed -i .org "s/circleci_version_number/${CIRCLE_BUILD_NUM}/g" config/config.default.json - run: name: Build docker image command: docker build -t ${di}:build_${CIRCLE_BUILD_NUM} -t ${di}:${CIRCLE_SHA1} -t ${di}:${dtag} . diff --git a/.eslintrc b/.eslintrc index 7dbccca..1d23fea 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,8 @@ }, "globals": { "FroalaEditor": "readonly", - "gapi": "readonly" + "gapi": "readonly", + "m": true }, "extends": "eslint:recommended", "env": { diff --git a/api/article/model.mjs b/api/article/model.mjs index fdbe406..af89e8c 100644 --- a/api/article/model.mjs +++ b/api/article/model.mjs @@ -76,6 +76,17 @@ const Article = bookshelf.createModel({ return result }) }, + + getFrontpageArticles() { + return this.query(qb => { + qb.orderBy('updated_at', 'DESC') + }) + .fetchPage({ + pageSize: 10, + page: 1, + withRelated: ['files', 'media', 'banner'], + }) + }, }) export default Article diff --git a/api/media/model.mjs b/api/media/model.mjs index e029cbe..1a9e944 100644 --- a/api/media/model.mjs +++ b/api/media/model.mjs @@ -44,7 +44,7 @@ const Media = bookshelf.createModel({ }, url() { - return `${Media.baseUrl}${this.get('large_image')}` + return `${Media.baseUrl}${this.get('medium_image')}` }, thumb() { diff --git a/api/page/model.mjs b/api/page/model.mjs index 480c324..30c1adb 100644 --- a/api/page/model.mjs +++ b/api/page/model.mjs @@ -61,6 +61,12 @@ const Page = bookshelf.createModel({ }) .fetch({ require, withRelated, ctx }) }, + getTree() { + return this.query(qb => { + qb.where({ parent_id: null }) + qb.select(['id', 'name', 'path']) + }).fetchAll({ withRelated: ['children'] }) + }, }) export default Page diff --git a/api/serve.mjs b/api/serve.mjs index 5f5ac3e..c651bf4 100644 --- a/api/serve.mjs +++ b/api/serve.mjs @@ -1,5 +1,82 @@ +import { readFileSync } from 'fs' import send from 'koa-send' +import dot from 'dot' import defaults from './defaults.mjs' +import config from './config.mjs' +import Page from './page/model.mjs' +import Article from './article/model.mjs' + +const body = readFileSync('./public/index.html').toString() +const bodyTemplate = dot.template(body) + +async function sendIndex(ctx, path) { + let tree = null + let data = null + let links = null + try { + tree = (await Page.getTree()).toJSON() + tree.forEach(item => ( + item.children = item.children.map(x => ( + { id: x.id, name: x.name, path: x.path } + )) + )) + if (path === '/') { + data = await Article.getFrontpageArticles() + + if (data.pagination.rowCount > 10) { + links = { + current: { title: 'Page 1' }, + next: { page: 2, title: 'Next' }, + last: { page: Math.ceil(data.pagination.rowCount / 10), title: 'Last' }, + } + } else { + links = { + current: { title: 'Page 1' }, + } + } + data = data.toJSON().map(x => ({ + id: x.id, + created_at: x.created_at, + path: x.path, + description: x.description, + name: x.name, + media: x.media && ({ + medium_url: x.media.medium_url, + small_url: x.media.small_url, + }) || null, + banner: x.banner && ({ + large_url: x.banner.large_url, + medium_url: x.banner.medium_url, + small_url: x.banner.small_url, + }) || null, + files: x.files && x.files.map(f => ({ + filename: f.filename, + url: f.url, + magnet: f.magnet, + meta: f.meta.torrent && ({ + torrent: { + files: f.meta.torrent.files.map(tf => ({ + name: tf.name, + size: tf.size, + })), + }, + }) || {}, + })) || [], + })) + } + } catch (e) { + ctx.log.error(e) + } + ctx.body = bodyTemplate({ + v: config.get('CIRCLECI_VERSION'), + tree: JSON.stringify(tree), + data: JSON.stringify(data), + links: JSON.stringify(links), + }) + ctx.set('Content-Length', Buffer.byteLength(ctx.body)) + ctx.set('Cache-Control', 'max-age=0') + ctx.set('Content-Type', 'text/html; charset=utf-8') +} export function serve(docRoot, pathname, options = {}) { options.root = docRoot @@ -22,9 +99,14 @@ export function serve(docRoot, pathname, options = {}) { opts = defaults({ maxage: 2592000 * 1000 }, opts) } + if (filepath === '/index.html') { + return sendIndex(ctx, '/') + } + return send(ctx, filepath, opts).catch((er) => { if (er.code === 'ENOENT' && er.status === 404) { - return send(ctx, '/index.html', options) + return sendIndex(ctx) + // return send(ctx, '/index.html', options) } }) } diff --git a/app/admin.js b/app/admin.js new file mode 100644 index 0000000..5286764 --- /dev/null +++ b/app/admin.js @@ -0,0 +1,15 @@ +const EditPage = require('./admin/editpage') +const AdminPages = require('./admin/pages') +const AdminArticles = require('./admin/articles') +const EditArticle = require('./admin/editarticle') +const AdminStaffList = require('./admin/stafflist') +const EditStaff = require('./admin/editstaff') + +window.addAdminRoutes = [ + ['/admin/pages', AdminPages], + ['/admin/pages/:key', EditPage], + ['/admin/articles', AdminArticles], + ['/admin/articles/:id', EditArticle], + ['/admin/staff', AdminStaffList], + ['/admin/staff/:id', EditStaff], +] diff --git a/app/admin.scss b/app/admin.scss new file mode 100644 index 0000000..003daec --- /dev/null +++ b/app/admin.scss @@ -0,0 +1,79 @@ +@import './_common'; + +.error { + font-size: 0.8em; + color: $secondary-dark-bg; + font-weight: bold; + padding-bottom: 20px; +} + +$bordercolor: $primary-bg; +$headcolor: $primary-light-bg; +$headtext: $primary-light-fg; + +.admin-wrapper table { + width: calc(100% - 20px); + margin: 10px; + border: solid 1px $bordercolor; + border-collapse: collapse; + border-spacing: 0; + font-size: 0.8em; + + thead th { + background-color: $headcolor; + border: solid 1px $bordercolor; + color: $headtext; + padding: 10px; + text-align: left; + } + tbody td { + text-align: left; + border: solid 1px $bordercolor; + color: $table-fg; + padding: 10px; + } + a, + a:visited, + a:hover { + text-decoration: none; + color: $secondary-dark-bg; + font-weight: bold; + } + + button { + color: $secondary-dark-bg; + background: transparent; + border: 1px solid $secondary-dark-bg; + } + + td.right, + th.right { + text-align: right; + } +} + +.floating-container { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: #00000099; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +@import 'admin/admin'; +@import 'widgets/admin'; + +.darkmodeon { + .maincontainer .admin-wrapper { + color: $main-fg; + } + + .error { + color: $dark_secondary-dark-bg; + } +} diff --git a/app/admin/admin.scss b/app/admin/admin.scss index bae0155..400f477 100644 --- a/app/admin/admin.scss +++ b/app/admin/admin.scss @@ -38,21 +38,3 @@ @import 'pages'; @import 'articles'; @import 'staff'; - -.darkmodeon { - .admin-wrapper { - background: $dark_primary-bg; - } - - .admin-actions { - background: $dark_primary-bg; - - span { - color: $dark_primary-fg; - } - - a { - color: $dark_secondary-light-bg; - } - } -} diff --git a/app/admin/articles.js b/app/admin/articles.js index f16e7ac..764e9c7 100644 --- a/app/admin/articles.js +++ b/app/admin/articles.js @@ -1,5 +1,3 @@ -const m = require('mithril') - const { getAllArticlesPagination, removeArticle } = require('../api/article') const { fetchPage } = require('../api/pagination') const Dialogue = require('../widgets/dialogue') diff --git a/app/admin/editarticle.js b/app/admin/editarticle.js index 05d708f..2fa6967 100644 --- a/app/admin/editarticle.js +++ b/app/admin/editarticle.js @@ -1,5 +1,3 @@ -const m = require('mithril') - const Authentication = require('../authentication') const FileUpload = require('../widgets/fileupload') const Froala = require('./froala') @@ -120,6 +118,9 @@ const EditArticle = { if (this.error) return this.article.description = vnode.state.froala && vnode.state.froala.html.get() || this.article.description + if (this.article.description) { + this.article.description = this.article.description.replace(/
]+data-f-id="pbf"[^>]+>[^>]+>[^>]+>[^>]+>/, '') + } this.loading = true diff --git a/app/admin/editpage.js b/app/admin/editpage.js index 5f9b406..9a1be15 100644 --- a/app/admin/editpage.js +++ b/app/admin/editpage.js @@ -1,5 +1,3 @@ -const m = require('mithril') - const Authentication = require('../authentication') const FileUpload = require('../widgets/fileupload') const Froala = require('./froala') @@ -98,6 +96,9 @@ const EditPage = { if (this.error) return this.page.description = vnode.state.froala ? vnode.state.froala.html.get() : this.page.description + if (this.page.description) { + this.page.description = this.page.description.replace(/
]+data-f-id="pbf"[^>]+>[^>]+>[^>]+>[^>]+>/, '')
+ }
this.loading = true
diff --git a/app/admin/editstaff.js b/app/admin/editstaff.js
index 6091178..32430e1 100644
--- a/app/admin/editstaff.js
+++ b/app/admin/editstaff.js
@@ -1,5 +1,3 @@
-const m = require('mithril')
-
const { createStaff, updateStaff, getStaff } = require('../api/staff')
const EditStaff = {
diff --git a/app/admin/froala.js b/app/admin/froala.js
index fc1b214..8fc706b 100644
--- a/app/admin/froala.js
+++ b/app/admin/froala.js
@@ -33,7 +33,7 @@ const Froala = {
element.setAttribute('href', Froala.files[i].url)
} else {
element = document.createElement('script')
- element.setAttribute('type','text/javascript')
+ element.setAttribute('type', 'text/javascript')
element.setAttribute('src', Froala.files[i].url)
}
element.onload = onload
diff --git a/app/admin/pages.js b/app/admin/pages.js
index dc56719..3923e48 100644
--- a/app/admin/pages.js
+++ b/app/admin/pages.js
@@ -1,6 +1,3 @@
-const m = require('mithril')
-
-const Authentication = require('../authentication')
const { getAllPages, removePage } = require('../api/page')
const Dialogue = require('../widgets/dialogue')
diff --git a/app/admin/stafflist.js b/app/admin/stafflist.js
index fc6c436..6347030 100644
--- a/app/admin/stafflist.js
+++ b/app/admin/stafflist.js
@@ -1,5 +1,3 @@
-const m = require('mithril')
-
const { getAllStaff, removeStaff } = require('../api/staff')
const Dialogue = require('../widgets/dialogue')
const Pages = require('../widgets/pages')
diff --git a/app/api/common.js b/app/api/common.js
index 11a5a84..e02a43e 100644
--- a/app/api/common.js
+++ b/app/api/common.js
@@ -1,4 +1,3 @@
-const m = require('mithril')
const Authentication = require('../authentication')
exports.sendRequest = function(options, isPagination) {
diff --git a/app/api/media.js b/app/api/media.js
index c790f12..2f1a7fd 100644
--- a/app/api/media.js
+++ b/app/api/media.js
@@ -1,4 +1,3 @@
-const m = require('mithril')
const { sendRequest } = require('./common')
exports.uploadMedia = function(file) {
diff --git a/app/api/page.js b/app/api/page.js
index 097857a..94a62e1 100644
--- a/app/api/page.js
+++ b/app/api/page.js
@@ -1,6 +1,6 @@
const { sendRequest } = require('./common')
-const Tree = []
+const Tree = window.__nfptree || []
exports.Tree = Tree
@@ -25,7 +25,7 @@ exports.createPage = function(body) {
})
}
-exports.getTree = function(body) {
+exports.getTree = function() {
return sendRequest({
method: 'GET',
url: '/api/pages?tree=true&includes=children&fields=id,name,path,children(id,name,path)',
diff --git a/app/app.scss b/app/app.scss
index b5dcc3d..ebf42b9 100644
--- a/app/app.scss
+++ b/app/app.scss
@@ -167,88 +167,11 @@ form {
}
}
-.fr-view {
- .clearfix::after{clear:both;display:block;content:"";height:0}.hide-by-clipping{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}img.fr-rounded,.fr-img-caption.fr-rounded img{border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box}img.fr-bordered,.fr-img-caption.fr-bordered img{border:solid 5px #CCC}img.fr-bordered{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fr-img-caption.fr-bordered img{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fr-view{word-wrap:break-word}.fr-view span[style~="color:"] a{color:inherit}.fr-view strong{font-weight:700}.fr-view table{border:none;border-collapse:collapse;empty-cells:show;max-width:100%}.fr-view table td{min-width:5px}.fr-view table.fr-dashed-borders td,.fr-view table.fr-dashed-borders th{border-style:dashed}.fr-view table.fr-alternate-rows tbody tr:nth-child(2n){background:whitesmoke}.fr-view table td,.fr-view table th{border:1px solid #DDD}.fr-view table td:empty,.fr-view table th:empty{height:20px}.fr-view table td.fr-highlighted,.fr-view table th.fr-highlighted{border:1px double red}.fr-view table td.fr-thick,.fr-view table th.fr-thick{border-width:2px}.fr-view table th{background:#ececec}.fr-view hr{clear:both;user-select:none;-o-user-select:none;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-ms-user-select:none;page-break-after:always}.fr-view .fr-file{position:relative}.fr-view .fr-file::after{position:relative;content:"\1F4CE";font-weight:normal}.fr-view pre{white-space:pre-wrap;word-wrap:break-word;overflow:visible}.fr-view[dir="rtl"] blockquote{border-left:none;border-right:solid 2px #5E35B1;margin-right:0;padding-right:5px;padding-left:0}.fr-view[dir="rtl"] blockquote blockquote{border-color:#00BCD4}.fr-view[dir="rtl"] blockquote blockquote blockquote{border-color:#43A047}.fr-view blockquote{border-left:solid 2px #5E35B1;margin-left:0;padding-left:5px;color:#5E35B1}.fr-view blockquote blockquote{border-color:#00BCD4;color:#00BCD4}.fr-view blockquote blockquote blockquote{border-color:#43A047;color:#43A047}.fr-view span.fr-emoticon{font-weight:normal;font-family:"Apple Color Emoji","Segoe UI Emoji","NotoColorEmoji","Segoe UI Symbol","Android Emoji","EmojiSymbols";display:inline;line-height:0}.fr-view span.fr-emoticon.fr-emoticon-img{background-repeat:no-repeat !important;font-size:inherit;height:1em;width:1em;min-height:20px;min-width:20px;display:inline-block;margin:-.1em .1em .1em;line-height:1;vertical-align:middle}.fr-view .fr-text-gray{color:#AAA !important}.fr-view .fr-text-bordered{border-top:solid 1px #222;border-bottom:solid 1px #222;padding:10px 0}.fr-view .fr-text-spaced{letter-spacing:1px}.fr-view .fr-text-uppercase{text-transform:uppercase}.fr-view .fr-class-highlighted{background-color:#ffff00}.fr-view .fr-class-code{border-color:#cccccc;border-radius:2px;-moz-border-radius:2px;-webkit-border-radius:2px;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box;background:#f5f5f5;padding:10px;font-family:"Courier New", Courier, monospace}.fr-view .fr-class-transparency{opacity:0.5}.fr-view img{position:relative;max-width:100%}.fr-view img.fr-dib{margin:5px auto;display:block;float:none;vertical-align:top}.fr-view img.fr-dib.fr-fil{margin-left:0;text-align:left}.fr-view img.fr-dib.fr-fir{margin-right:0;text-align:right}.fr-view img.fr-dii{display:inline-block;float:none;vertical-align:bottom;margin-left:5px;margin-right:5px;max-width:calc(100% - (2 * 5px))}.fr-view img.fr-dii.fr-fil{float:left;margin:5px 5px 5px 0;max-width:calc(100% - 5px)}.fr-view img.fr-dii.fr-fir{float:right;margin:5px 0 5px 5px;max-width:calc(100% - 5px)}.fr-view span.fr-img-caption{position:relative;max-width:100%}.fr-view span.fr-img-caption.fr-dib{margin:5px auto;display:block;float:none;vertical-align:top}.fr-view span.fr-img-caption.fr-dib.fr-fil{margin-left:0;text-align:left}.fr-view span.fr-img-caption.fr-dib.fr-fir{margin-right:0;text-align:right}.fr-view span.fr-img-caption.fr-dii{display:inline-block;float:none;vertical-align:bottom;margin-left:5px;margin-right:5px;max-width:calc(100% - (2 * 5px))}.fr-view span.fr-img-caption.fr-dii.fr-fil{float:left;margin:5px 5px 5px 0;max-width:calc(100% - 5px)}.fr-view span.fr-img-caption.fr-dii.fr-fir{float:right;margin:5px 0 5px 5px;max-width:calc(100% - 5px)}.fr-view .fr-video{text-align:center;position:relative}.fr-view .fr-video.fr-rv{padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden}.fr-view .fr-video.fr-rv>iframe,.fr-view .fr-video.fr-rv object,.fr-view .fr-video.fr-rv embed{position:absolute !important;top:0;left:0;width:100%;height:100%}.fr-view .fr-video>*{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;max-width:100%;border:none}.fr-view .fr-video.fr-dvb{display:block;clear:both}.fr-view .fr-video.fr-dvb.fr-fvl{text-align:left}.fr-view .fr-video.fr-dvb.fr-fvr{text-align:right}.fr-view .fr-video.fr-dvi{display:inline-block}.fr-view .fr-video.fr-dvi.fr-fvl{float:left}.fr-view .fr-video.fr-dvi.fr-fvr{float:right}.fr-view a.fr-strong{font-weight:700}.fr-view a.fr-green{color:green}.fr-view .fr-img-caption{text-align:center}.fr-view .fr-img-caption .fr-img-wrap{padding:0;display:inline-block;margin:auto;text-align:center;width:100%}.fr-view .fr-img-caption .fr-img-wrap img{display:block;margin:auto;width:100%}.fr-view .fr-img-caption .fr-img-wrap>span{margin:auto;display:block;padding:5px 5px 10px;font-size:14px;font-weight:initial;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-opacity:0.9;-moz-opacity:0.9;opacity:0.9;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";width:100%;text-align:center}.fr-view button.fr-rounded,.fr-view input.fr-rounded,.fr-view textarea.fr-rounded{border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box}.fr-view button.fr-large,.fr-view input.fr-large,.fr-view textarea.fr-large{font-size:24px}a.fr-view.fr-strong{font-weight:700}a.fr-view.fr-green{color:green}img.fr-view{position:relative;max-width:100%}img.fr-view.fr-dib{margin:5px auto;display:block;float:none;vertical-align:top}img.fr-view.fr-dib.fr-fil{margin-left:0;text-align:left}img.fr-view.fr-dib.fr-fir{margin-right:0;text-align:right}img.fr-view.fr-dii{display:inline-block;float:none;vertical-align:bottom;margin-left:5px;margin-right:5px;max-width:calc(100% - (2 * 5px))}img.fr-view.fr-dii.fr-fil{float:left;margin:5px 5px 5px 0;max-width:calc(100% - 5px)}img.fr-view.fr-dii.fr-fir{float:right;margin:5px 0 5px 5px;max-width:calc(100% - 5px)}span.fr-img-caption.fr-view{position:relative;max-width:100%}span.fr-img-caption.fr-view.fr-dib{margin:5px auto;display:block;float:none;vertical-align:top}span.fr-img-caption.fr-view.fr-dib.fr-fil{margin-left:0;text-align:left}span.fr-img-caption.fr-view.fr-dib.fr-fir{margin-right:0;text-align:right}span.fr-img-caption.fr-view.fr-dii{display:inline-block;float:none;vertical-align:bottom;margin-left:5px;margin-right:5px;max-width:calc(100% - (2 * 5px))}span.fr-img-caption.fr-view.fr-dii.fr-fil{float:left;margin:5px 5px 5px 0;max-width:calc(100% - 5px)}span.fr-img-caption.fr-view.fr-dii.fr-fir{float:right;margin:5px 0 5px 5px;max-width:calc(100% - 5px)}
- h1, h2, h3, h4, h5, h6, p, dl, ol, ul {
- margin: 0 0 1em !important;
- }
-
- a { color: $secondary-dark-bg; }
- dt { font-weight: bold; }
- ol { list-style-type: decimal; padding-left: 40px; }
- ul { list-style-type: disc; padding-left: 40px; }
- h1 { font-size: 1.8em; font-weight: bold; }
- h2 { font-size: 1.6em; font-weight: bold; }
- h3 { font-size: 1.4em; font-weight: bold; }
- h4 { font-size: 1.2em; font-weight: bold; }
- h5 { font-size: 1.0em; font-weight: bold; }
- h6 { font-size: 0.8em; font-weight: bold; }
- hr { width: 100%; }
-
-}
-
-$bordercolor: $primary-bg;
-$headcolor: $primary-light-bg;
-$headtext: $primary-light-fg;
-
-table {
- width: calc(100% - 20px);
- margin: 10px;
- border: solid 1px $bordercolor;
- border-collapse: collapse;
- border-spacing: 0;
- font-size: 0.8em;
-
- thead th {
- background-color: $headcolor;
- border: solid 1px $bordercolor;
- color: $headtext;
- padding: 10px;
- text-align: left;
- }
- tbody td {
- text-align: left;
- border: solid 1px $bordercolor;
- color: $table-fg;
- padding: 10px;
- }
- a,
- a:visited,
- a:hover {
- text-decoration: none;
- color: $secondary-dark-bg;
- font-weight: bold;
- }
-
- button {
- color: $secondary-dark-bg;
- background: transparent;
- border: 1px solid $secondary-dark-bg;
- }
-
- td.right,
- th.right {
- text-align: right;
- }
-}
-
-.floating-container {
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- background: #00000099;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
-}
+@import 'froala';
@import 'menu/menu';
@import 'footer/footer';
@import 'login/login';
-@import 'admin/admin';
@import 'widgets/common';
@import 'pages/page';
@import 'article/article';
@@ -264,10 +187,17 @@ table {
color: $dark_secondary-dark-bg;
}
- article {
+ .fr-view blockquote {
+ border-left-color: $dark_main-fg;
+ color: $dark_main-fg;
+ }
+
+ article.article,
+ article.login,
+ article.page {
header {
h1 {
- color: $dark_title-fg;
+ // color: $dark_title-fg;
}
span {
@@ -282,7 +212,7 @@ table {
}
}
- form {
+ .login form {
input[type=text],
input[type=password],
select,
@@ -312,32 +242,4 @@ table {
.fr-view {
a { color: $dark_secondary-dark-bg; }
}
-
- $dark_bordercolor: $dark_primary-bg;
- $dark_headcolor: $dark_primary-light-bg;
- $dark_headtext: $dark_primary-light-fg;
-
- table {
- border: solid 1px $dark_bordercolor;
-
- thead th {
- background-color: $dark_headcolor;
- border: solid 1px $dark_bordercolor;
- color: $dark_headtext;
- }
- tbody td {
- border: solid 1px $dark_bordercolor;
- color: $dark_table-fg;
- }
- a,
- a:visited,
- a:hover {
- color: $dark_secondary-dark-bg;
- }
-
- button {
- color: $dark_secondary-dark-bg;
- border: 1px solid $dark_secondary-dark-bg;
- }
- }
}
diff --git a/app/article/article.js b/app/article/article.js
index c376123..7df4590 100644
--- a/app/article/article.js
+++ b/app/article/article.js
@@ -54,6 +54,7 @@ const Article = {
m('.fr-view', [
this.article.media
? m('a.cover', {
+ rel: 'noopener',
href: this.article.media.url,
}, m('img', { src: this.article.media.medium_url, alt: 'Cover image for ' + this.article.name }))
: null,
diff --git a/app/authentication.js b/app/authentication.js
index 12fa25c..992f8be 100644
--- a/app/authentication.js
+++ b/app/authentication.js
@@ -1,30 +1,35 @@
-const m = require('mithril')
-
const storageName = 'logintoken'
-const loadingListeners = []
-
-window.googleLoaded = function() {
- Authentication.loadedGoogle = true
- while (Authentication.loadingListeners.length) {
- Authentication.loadingListeners.pop()()
- }
-}
const Authentication = {
currentUser: null,
+ isAdmin: false,
loadedGoogle: false,
loadingGoogle: false,
loadingListeners: [],
+ authListeners: [],
updateToken: function(token) {
if (!token) return Authentication.clearToken()
localStorage.setItem(storageName, token)
Authentication.currentUser = JSON.parse(atob(token.split('.')[1]))
+
+ if (Authentication.authListeners.length) {
+ Authentication.authListeners.forEach(function(x) { x(Authentication.currentUser) })
+ }
},
clearToken: function() {
Authentication.currentUser = null
localStorage.removeItem(storageName)
+ Authentication.isAdmin = false
+ },
+
+ addEvent: function(event) {
+ Authentication.authListeners.push(event)
+ },
+
+ setAdmin: function(item) {
+ Authentication.isAdmin = item
},
createGoogleScript: function() {
@@ -50,6 +55,15 @@ const Authentication = {
},
}
+if (!window.googleLoaded) {
+ window.googleLoaded = function() {
+ Authentication.loadedGoogle = true
+ while (Authentication.loadingListeners.length) {
+ Authentication.loadingListeners.pop()()
+ }
+ }
+}
+
Authentication.updateToken(localStorage.getItem(storageName))
module.exports = Authentication
diff --git a/app/darkmode.js b/app/darkmode.js
index a575806..a230631 100644
--- a/app/darkmode.js
+++ b/app/darkmode.js
@@ -10,7 +10,7 @@ const Darkmode = {
Darkmode.darkIsOn = true
} else {
localStorage.removeItem(storageName)
- document.body.className = ''
+ document.body.className = 'daymode'
Darkmode.darkIsOn = false
}
},
diff --git a/app/footer/footer.js b/app/footer/footer.js
index 8845585..3a469c2 100644
--- a/app/footer/footer.js
+++ b/app/footer/footer.js
@@ -1,7 +1,6 @@
const m = require('mithril')
const { Tree } = require('../api/page')
const Authentication = require('../authentication')
-const Darkmode = require('../darkmode')
const Footer = {
oninit: function(vnode) {
@@ -9,11 +8,6 @@ const Footer = {
},
view: function() {
- var pixelRatio = window.devicePixelRatio || 1
- var darkPrefix = ''
- if (Darkmode.darkIsOn) {
- darkPrefix = 'dark_'
- }
return [
m('div.sitemap', [
m('div', 'Sitemap'),
@@ -31,16 +25,15 @@ const Footer = {
!Authentication.currentUser
? m(m.route.Link, { class: 'root', href: '/login' }, 'Login')
: null,
- m('div.meta', ['©'
- + this.year
- + ' NFP Encodes - nfp@nfp.moe - ',
- m('a', { href: 'https://www.iubenda.com/privacy-policy/31076050', target: '_blank' }, 'Privacy Policy'),
- ' (Fuck EU)',
- ])
+ m('div.meta', [
+ '©'
+ + this.year
+ + ' NFP Encodes - nfp@nfp.moe - ',
+ m('a', { rel: 'noopener', href: 'https://www.iubenda.com/privacy-policy/31076050', target: '_blank' }, 'Privacy Policy'),
+ ' (Fuck EU)',
+ ]),
]),
- m('div.footer-logo', { style: {
- 'background-image': pixelRatio > 1 ? 'url("/assets/img/' + darkPrefix + 'tsun.jpg")' : 'url("/assets/img/' + darkPrefix + 'tsun_small.jpg")'
- } }),
+ m('div.footer-logo'),
]
},
}
diff --git a/app/footer/footer.scss b/app/footer/footer.scss
index 41d2679..2f92f4d 100644
--- a/app/footer/footer.scss
+++ b/app/footer/footer.scss
@@ -73,3 +73,35 @@ footer {
}
}
}
+
+.daymode footer .footer-logo {
+ background-image: url("/assets/img/tsun_small.jpg");
+}
+
+.darkmodeon footer .footer-logo {
+ background-image: url("/assets/img/dark_tsun_small.jpg");
+}
+
+@media
+only screen and (-webkit-min-device-pixel-ratio: 2),
+only screen and ( min--moz-device-pixel-ratio: 2),
+only screen and ( -o-min-device-pixel-ratio: 2/1),
+only screen and ( min-device-pixel-ratio: 2),
+only screen and ( min-resolution: 192dpi),
+only screen and ( min-resolution: 2dppx) {
+ .daymode .footer-logo {
+ background-image: url("/assets/img/tsun.jpg");
+ }
+
+ .darkmodeon .footer-logo {
+ background-image: url("/assets/img/dark_tsun.jpg");
+ }
+}
+
+@media (pointer:coarse) {
+ footer .sitemap a.root,
+ footer .sitemap a.child {
+ // padding: 10px 10px;
+ // display: inline-block;
+ }
+}
diff --git a/app/froala.scss b/app/froala.scss
new file mode 100644
index 0000000..20920c8
--- /dev/null
+++ b/app/froala.scss
@@ -0,0 +1,208 @@
+.fr-view {
+ word-wrap: break-word;
+
+ .clearfix::after {
+ clear: both;
+ display: block;
+ content: "";
+ height: 0
+ }
+ h1, h2, h3, h4, h5, h6, p, dl, ol, ul {
+ margin: 0 0 1em !important;
+ }
+
+ blockquote {
+ border-left: solid 2px $main-fg;
+ margin-left: 0;
+ padding-left: 5px;
+ color: $main-fg;
+ }
+
+ .hide-by-clipping {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0
+ }
+
+ img.fr-rounded,
+ .fr-img-caption.fr-rounded img {
+ border-radius: 10px;
+ -moz-border-radius: 10px;
+ -webkit-border-radius: 10px;
+ -moz-background-clip: padding;
+ -webkit-background-clip: padding-box;
+ background-clip: padding-box
+ }
+
+ img.fr-bordered,
+ .fr-img-caption.fr-bordered img {
+ border: solid 5px #CCC
+ }
+
+ img.fr-bordered {
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box
+ }
+
+ .fr-img-caption.fr-bordered img {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box
+ }
+
+ span.fr-emoticon {
+ font-weight: normal;
+ font-family: "Apple Color Emoji", "Segoe UI Emoji", "NotoColorEmoji", "Segoe UI Symbol", "Android Emoji", "EmojiSymbols";
+ display: inline;
+ line-height: 0
+ }
+
+ img {
+ position: relative;
+ max-width: 100%
+ }
+
+ img.fr-dib {
+ margin: 5px auto;
+ display: block;
+ float: none;
+ vertical-align: top
+ }
+
+ img.fr-dib.fr-fil {
+ margin-left: 0;
+ text-align: left
+ }
+
+ img.fr-dib.fr-fir {
+ margin-right: 0;
+ text-align: right
+ }
+
+ img.fr-dii {
+ display: inline-block;
+ float: none;
+ vertical-align: bottom;
+ margin-left: 5px;
+ margin-right: 5px;
+ max-width: calc(100% - (2 * 5px))
+ }
+
+ img.fr-dii.fr-fil {
+ float: left;
+ margin: 5px 5px 5px 0;
+ max-width: calc(100% - 5px)
+ }
+
+ img.fr-dii.fr-fir {
+ float: right;
+ margin: 5px 0 5px 5px;
+ max-width: calc(100% - 5px)
+ }
+
+ span.fr-img-caption {
+ position: relative;
+ max-width: 100%
+ }
+
+ span.fr-img-caption.fr-dib {
+ margin: 5px auto;
+ display: block;
+ float: none;
+ vertical-align: top
+ }
+
+ span.fr-img-caption.fr-dib.fr-fil {
+ margin-left: 0;
+ text-align: left
+ }
+
+ span.fr-img-caption.fr-dib.fr-fir {
+ margin-right: 0;
+ text-align: right
+ }
+
+ span.fr-img-caption.fr-dii {
+ display: inline-block;
+ float: none;
+ vertical-align: bottom;
+ margin-left: 5px;
+ margin-right: 5px;
+ max-width: calc(100% - (2 * 5px))
+ }
+
+ span.fr-img-caption.fr-dii.fr-fil {
+ float: left;
+ margin: 5px 5px 5px 0;
+ max-width: calc(100% - 5px)
+ }
+
+ span.fr-img-caption.fr-dii.fr-fir {
+ float: right;
+ margin: 5px 0 5px 5px;
+ max-width: calc(100% - 5px)
+ }
+
+ a.fr-strong {
+ font-weight: 700
+ }
+
+ a.fr-green {
+ color: green
+ }
+
+ .fr-img-caption {
+ text-align: center
+ }
+
+ .fr-img-caption .fr-img-wrap {
+ padding: 0;
+ display: inline-block;
+ margin: auto;
+ text-align: center;
+ width: 100%
+ }
+
+ .fr-img-caption .fr-img-wrap img {
+ display: block;
+ margin: auto;
+ width: 100%
+ }
+
+ .fr-img-caption .fr-img-wrap>span {
+ margin: auto;
+ display: block;
+ padding: 5px 5px 10px;
+ font-size: 14px;
+ font-weight: initial;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-opacity: 0.9;
+ -moz-opacity: 0.9;
+ opacity: 0.9;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
+ width: 100%;
+ text-align: center
+ }
+
+ a { color: $secondary-dark-bg; }
+ dt { font-weight: bold; }
+ ol { list-style-type: decimal; padding-left: 40px; }
+ ul { list-style-type: disc; padding-left: 40px; }
+ h1 { font-size: 1.8em; font-weight: bold; }
+ h2 { font-size: 1.6em; font-weight: bold; }
+ h3 { font-size: 1.4em; font-weight: bold; }
+ h4 { font-size: 1.2em; font-weight: bold; }
+ h5 { font-size: 1.0em; font-weight: bold; }
+ h6 { font-size: 0.8em; font-weight: bold; }
+ hr { width: 100%; }
+ strong { font-weight: 700 }
+
+}
diff --git a/app/frontpage/frontpage.js b/app/frontpage/frontpage.js
index 2f7b219..b1a26ce 100644
--- a/app/frontpage/frontpage.js
+++ b/app/frontpage/frontpage.js
@@ -5,15 +5,25 @@ const { getAllArticlesPagination } = require('../api/article')
const { fetchPage } = require('../api/pagination')
const Pages = require('../widgets/pages')
const Newsitem = require('../widgets/newsitem')
-const Darkmode = require('../darkmode')
-module.exports = {
+const Frontpage = {
oninit: function(vnode) {
this.error = ''
this.loading = false
this.featured = null
this.links = null
- this.fetchArticles(vnode)
+
+ if (window.__nfpdata
+ && window.__nfplinks) {
+ this.links = window.__nfplinks
+ this.articles = window.__nfpdata
+ this.lastpage = '1'
+ window.__nfpdata = null
+ window.__nfplinks = null
+ Frontpage.processFeatured(vnode, this.articles)
+ } else {
+ this.fetchArticles(vnode)
+ }
},
onupdate: function(vnode) {
@@ -30,19 +40,14 @@ module.exports = {
this.lastpage = m.route.param('page') || '1'
return fetchPage(getAllArticlesPagination({
- per_page: 10,
+ per_page: 2,
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]
- }
- }
+ Frontpage.processFeatured(vnode, result.data)
})
.catch(function(err) {
vnode.state.error = err.message
@@ -53,19 +58,18 @@ module.exports = {
})
},
+ processFeatured: function(vnode, data) {
+ for (var i = data.length - 1; i >= 0; i--) {
+ if (data[i].banner) {
+ vnode.state.featured = data[i]
+ }
+ }
+ },
+
view: function(vnode) {
var deviceWidth = window.innerWidth
var bannerPath = ''
- var asuna_side = ''
-
- if (deviceWidth > 800) {
- if (Darkmode.darkIsOn) {
- asuna_side = '/assets/img/dark_asuna_frontpage.jpg'
- } else {
- asuna_side = '/assets/img/asuna_frontpage.jpg'
- }
- }
if (this.featured && this.featured.banner) {
var pixelRatio = window.devicePixelRatio || 1
@@ -75,12 +79,12 @@ module.exports = {
|| (deviceWidth < 600 && pixelRatio > 1)) {
bannerPath = this.featured.banner.medium_url
} else {
- bannerPath = this.featured.banner.url
+ bannerPath = this.featured.banner.large_url
}
}
return [
- (this.featured && this.featured.banner
+ (bannerPath
? m('a.frontpage-banner', {
href: '/article/' + this.featured.path,
style: { 'background-image': 'url("' + bannerPath + '")' },
@@ -103,7 +107,7 @@ module.exports = {
]
}),
]),
- asuna_side ? m('img', { src: asuna_side, alt: 'Asuna standing tall welcomes you' }) : null,
+ m('div.asunaside'),
]),
m('.frontpage-news', [
(this.loading
@@ -121,3 +125,5 @@ module.exports = {
]
},
}
+
+module.exports = Frontpage
diff --git a/app/frontpage/frontpage.scss b/app/frontpage/frontpage.scss
index be63ba2..11e3251 100644
--- a/app/frontpage/frontpage.scss
+++ b/app/frontpage/frontpage.scss
@@ -38,10 +38,6 @@ frontpage {
display: flex;
flex-direction: column;
- img {
- align-self: center;
- }
-
.categories {
padding: 10px 10px 20px;
margin-bottom: 20px;
@@ -89,6 +85,10 @@ frontpage {
newsitem {
margin-bottom: 30px;
}
+
+ .asunaside {
+ display: none;
+ }
}
@media screen and (max-width: 1000px){
@@ -113,6 +113,26 @@ frontpage {
}
}
+@media screen and (min-width: 800px){
+ frontpage .asunaside {
+ display: block;
+ width: 200px;
+ height: 480px;
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: top left;
+ align-self: center;
+ }
+
+ .daymode frontpage .asunaside {
+ background-image: url("/assets/img/asuna_frontpage.jpg");
+ }
+
+ .darkmodeon frontpage .asunaside {
+ background-image: url("/assets/img/dark_asuna_frontpage.jpg");
+ }
+}
+
@media screen and (max-width: 480px){
.frontpage-banner {
width: 100%;
diff --git a/app/index.js b/app/index.js
index bb39775..604c0c8 100644
--- a/app/index.js
+++ b/app/index.js
@@ -1,4 +1,5 @@
const m = require('mithril')
+window.m = m
m.route.prefix = ''
@@ -7,31 +8,72 @@ const Footer = require('./footer/footer')
const Frontpage = require('./frontpage/frontpage')
const Login = require('./login/login')
const Logout = require('./login/logout')
-const EditPage = require('./admin/editpage')
const Page = require('./pages/page')
const Article = require('./article/article')
-const AdminPages = require('./admin/pages')
-const AdminArticles = require('./admin/articles')
-const EditArticle = require('./admin/editarticle')
-const AdminStaffList = require('./admin/stafflist')
-const EditStaff = require('./admin/editstaff')
+const Authentication = require('./authentication')
const menuRoot = document.getElementById('nav')
const mainRoot = document.getElementById('main')
const footerRoot = document.getElementById('footer')
-m.route(mainRoot, '/', {
+const allRoutes = {
'/': Frontpage,
'/login': Login,
'/logout': Logout,
'/page/:id': Page,
'/article/:id': Article,
- '/admin/pages': AdminPages,
- '/admin/pages/:key': EditPage,
- '/admin/articles': AdminArticles,
- '/admin/articles/:id': EditArticle,
- '/admin/staff': AdminStaffList,
- '/admin/staff/:id': EditStaff,
-})
+}
+
+m.route(mainRoot, '/', allRoutes)
m.mount(menuRoot, Menu)
m.mount(footerRoot, Footer)
+
+let loadingAdmin = false
+let loadedAdmin = false
+let loaded = 0
+
+const onLoaded = function() {
+ loaded++
+ if (loaded < 2) return
+
+ if (window.addAdminRoutes) {
+ window.addAdminRoutes.forEach(function (item) {
+ allRoutes[item[0]] = item[1]
+ })
+ m.route(mainRoot, '/', allRoutes)
+ }
+
+ Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.level >= 10)
+ loadedAdmin = true
+ m.redraw()
+}
+
+const loadAdmin = function(user) {
+ if (loadingAdmin) {
+ if (loadedAdmin) {
+ Authentication.setAdmin(user && user.level >= 10)
+ }
+ return
+ }
+ if (!user || user.level < 10) return
+
+ loadingAdmin = true
+
+ let element = document.createElement('link')
+ element.setAttribute('rel', 'stylesheet')
+ element.setAttribute('type', 'text/css')
+ element.setAttribute('href', '/assets/admin.css')
+ element.onload = onLoaded
+ document.getElementsByTagName('head')[0].appendChild(element)
+
+ element = document.createElement('script')
+ element.setAttribute('type', 'text/javascript')
+ element.setAttribute('src', '/assets/admin.js')
+ element.onload = onLoaded
+ document.body.appendChild(element)
+}
+
+Authentication.addEvent(loadAdmin)
+if (Authentication.currentUser) {
+ loadAdmin(Authentication.currentUser)
+}
diff --git a/app/menu/menu.js b/app/menu/menu.js
index 2a17057..a4b5fc9 100644
--- a/app/menu/menu.js
+++ b/app/menu/menu.js
@@ -18,11 +18,13 @@ const Menu = {
oninit: function(vnode) {
Menu.onbeforeupdate()
+ if (Tree.length) return
+
Menu.loading = true
getTree()
.then(function(results) {
- Tree.splice(0, Tree.Length)
+ Tree.splice(0, Tree.length)
Tree.push.apply(Tree, results)
})
.catch(function(err) {
@@ -35,13 +37,10 @@ const Menu = {
},
view: function() {
- var pixelRatio = window.devicePixelRatio || 1
return [
m('div.top', [
m(m.route.Link,
- { href: '/', class: 'logo', style: {
- 'background-image': pixelRatio > 1 ? 'url("/assets/img/logo.jpg")' : 'url("/assets/img/logo_small.jpg")'
- } },
+ { href: '/', class: 'logo' },
m('h2', 'NFP Moe')
),
m('aside', Authentication.currentUser ? [
@@ -51,16 +50,16 @@ const Menu = {
(Darkmode.darkIsOn
? m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, false) }, 'Day mode')
: m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, true) }, 'Night mode')
- )
+ ),
]),
- (Authentication.currentUser.level >= 10
+ (Authentication.isAdmin
? m('div.adminlinks', [
m(m.route.Link, { href: '/admin/articles/add' }, 'Create article'),
m(m.route.Link, { href: '/admin/articles' }, 'Articles'),
m(m.route.Link, { href: '/admin/pages' }, 'Pages'),
m(m.route.Link, { hidden: Authentication.currentUser.level < 100, href: '/admin/staff' }, 'Staff'),
])
- : null
+ : (Authentication.currentUser.level > 10 ? m('div.loading-spinner') : null)
),
] : (Darkmode.darkIsOn
? m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, false) }, 'Day mode')
diff --git a/app/menu/menu.scss b/app/menu/menu.scss
index 9aded18..6c5b6a9 100644
--- a/app/menu/menu.scss
+++ b/app/menu/menu.scss
@@ -26,6 +26,7 @@
display: flex;
color: $primary-dark-fg;
text-decoration: none;
+ background-image: url("/assets/img/logo_small.jpg");
}
h2 {
@@ -74,6 +75,11 @@
min-width: 100px;
}
}
+
+ .loading-spinner {
+ position: relative;
+ width: 200px;
+ }
}
}
@@ -121,6 +127,18 @@
}
}
+@media
+only screen and (-webkit-min-device-pixel-ratio: 2),
+only screen and ( min--moz-device-pixel-ratio: 2),
+only screen and ( -o-min-device-pixel-ratio: 2/1),
+only screen and ( min-device-pixel-ratio: 2),
+only screen and ( min-resolution: 192dpi),
+only screen and ( min-resolution: 2dppx) {
+ #nav .top a.logo {
+ background-image: url("/assets/img/logo.jpg");
+ }
+}
+
.darkmodeon {
#nav {
.top {
diff --git a/app/widgets/admin.scss b/app/widgets/admin.scss
new file mode 100644
index 0000000..9edc89a
--- /dev/null
+++ b/app/widgets/admin.scss
@@ -0,0 +1,133 @@
+
+fileupload {
+ position: relative;
+ display: flex;
+ align-items: stretch;
+ flex-direction: column;
+ justify-content: stretch;
+
+ .showicon,
+ .showbordericon,
+ .display {
+ flex-grow: 2;
+ }
+
+ .showbordericon {
+ border: 3px solid $title-fg;
+ border-style: dashed;
+ background-image: url('./img/upload.svg');
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 50px;
+ }
+
+ .showicon {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ width: 50px;
+ height: 50px;
+ background-image: url('./img/upload.svg');
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ }
+
+ img {
+ max-width: 600px;
+ width: 100%;
+ align-self: center;
+ min-height: 100px;
+ }
+
+ .display {
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+
+ .loading-spinner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #33333388;
+ width: 100%;
+ }
+
+ input {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ opacity: 0.01;
+ width: 100%;
+ cursor: pointer;
+ text-indent: -9999px;
+ 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 {
+ background: white;
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ width: calc(100% - 40px);
+ max-width: 500px;
+ color: $main-fg;
+
+ h2 {
+ background: $secondary-dark-bg;
+ color: $secondary-dark-fg;
+ font-size: 1.5em;
+ padding: 10px;
+ }
+
+ p {
+ padding: 10px;
+ }
+
+ .buttons {
+ display: flex;
+ justify-content: space-around;
+ padding: 10px;
+ }
+
+ button {
+ border: 1px solid $secondary-dark-bg;
+ background: transparent;
+ color: $secondary-dark-bg;
+ padding: 5px 15px;
+ min-width: 150px;
+ }
+
+ button.alert {
+ border-color: red;
+ color: red;
+ }
+
+ button.cancel {
+ border-color: #999;
+ color: #999;
+ }
+}
diff --git a/app/widgets/common.scss b/app/widgets/common.scss
index c94c620..9c1ce5f 100644
--- a/app/widgets/common.scss
+++ b/app/widgets/common.scss
@@ -1,137 +1,4 @@
-
-fileupload {
- position: relative;
- display: flex;
- align-items: stretch;
- flex-direction: column;
- justify-content: stretch;
-
- .showicon,
- .showbordericon,
- .display {
- flex-grow: 2;
- }
-
- .showbordericon {
- border: 3px solid $title-fg;
- border-style: dashed;
- background-image: url('./img/upload.svg');
- background-position: center;
- background-repeat: no-repeat;
- background-size: 50px;
- }
-
- .showicon {
- position: absolute;
- top: 5px;
- right: 5px;
- width: 50px;
- height: 50px;
- background-image: url('./img/upload.svg');
- background-position: center;
- background-repeat: no-repeat;
- background-size: contain;
- }
-
- img {
- max-width: 600px;
- width: 100%;
- align-self: center;
- min-height: 100px;
- }
-
- .display {
- background-size: cover;
- background-repeat: no-repeat;
- background-position: center;
- }
-
- .loading-spinner {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: #33333388;
- width: 100%;
- }
-
- input {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- opacity: 0.01;
- width: 100%;
- cursor: pointer;
- text-indent: -9999px;
- 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 {
- background: white;
- display: flex;
- flex-direction: column;
- text-align: center;
- width: calc(100% - 40px);
- max-width: 500px;
-
- h2 {
- background: $secondary-dark-bg;
- color: $secondary-dark-fg;
- font-size: 1.5em;
- padding: 10px;
- }
-
- p {
- padding: 10px;
- }
-
- .buttons {
- display: flex;
- justify-content: space-around;
- padding: 10px;
- }
-
- button {
- border: 1px solid $secondary-dark-bg;
- background: transparent;
- color: $secondary-dark-bg;
- padding: 5px 15px;
- min-width: 150px;
- }
-
- button.alert {
- border-color: red;
- color: red;
- }
-
- button.cancel {
- border-color: #999;
- color: #999;
- }
-}
-
newsentry {
display: flex;
color: $meta-fg;
@@ -275,6 +142,10 @@ newsitem {
flex: 2 1 auto;
padding: 0 5px 5px;
+ &.extrapadding {
+ padding: 0 15px 5px;
+ }
+
h3 {
margin-bottom: 0 !important;
font-size: 1.3em;
@@ -342,28 +213,14 @@ pages {
.newsitemcontent {
flex-direction: column;
}
+
+ .entrycontent.extrapadding {
+ padding: 0 5px 5px;
+ }
}
}
.darkmodeon {
- fileupload {
- .showbordericon {
- border: 3px solid $dark_title-fg;
- }
- }
-
- dialogue {
- h2 {
- background: $dark_secondary-dark-bg;
- color: $dark_secondary-dark-fg;
- }
-
- button {
- border: 1px solid $dark_secondary-dark-bg;
- color: $dark_secondary-dark-bg;
- }
- }
-
newsentry {
color: $dark_meta-fg;
diff --git a/app/widgets/dialogue.js b/app/widgets/dialogue.js
index fcbba92..e2ac9a1 100644
--- a/app/widgets/dialogue.js
+++ b/app/widgets/dialogue.js
@@ -1,5 +1,3 @@
-const m = require('mithril')
-
const Dialogue = {
view: function(vnode) {
return m('div.floating-container', {
@@ -10,7 +8,7 @@ const Dialogue = {
m('div.buttons', [
m('button', { class: vnode.attrs.yesclass || '', onclick: vnode.attrs.onyes }, vnode.attrs.yes),
m('button', { class: vnode.attrs.noclass || '', onclick: vnode.attrs.onno }, vnode.attrs.no),
- ])
+ ]),
])
)
},
diff --git a/app/widgets/fileinfo.js b/app/widgets/fileinfo.js
index 546a47b..888417f 100644
--- a/app/widgets/fileinfo.js
+++ b/app/widgets/fileinfo.js
@@ -1,5 +1,3 @@
-const m = require('mithril')
-
const Fileinfo = {
getPrefix(vnode) {
if (!vnode.attrs.file.filename.endsWith('.torrent')) {
@@ -49,6 +47,7 @@ const Fileinfo = {
m('span.prefix', this.getPrefix(vnode) + ':'),
m('a', {
target: '_blank',
+ rel: 'noopener',
href: vnode.attrs.file.url,
}, this.getDownloadName(vnode)),
vnode.attrs.file.magnet
diff --git a/app/widgets/fileupload.js b/app/widgets/fileupload.js
index 8ee8ec6..b1087b6 100644
--- a/app/widgets/fileupload.js
+++ b/app/widgets/fileupload.js
@@ -1,4 +1,3 @@
-const m = require('mithril')
const { uploadMedia } = require('../api/media')
const FileUpload = {
diff --git a/app/widgets/newsentry.js b/app/widgets/newsentry.js
index fa84097..848db57 100644
--- a/app/widgets/newsentry.js
+++ b/app/widgets/newsentry.js
@@ -1,4 +1,3 @@
-const m = require('mithril')
const Fileinfo = require('./fileinfo')
const Newsentry = {
diff --git a/app/widgets/newsitem.js b/app/widgets/newsitem.js
index 9b82a17..66c4b74 100644
--- a/app/widgets/newsitem.js
+++ b/app/widgets/newsitem.js
@@ -1,4 +1,3 @@
-const m = require('mithril')
const Fileinfo = require('./fileinfo')
const Newsitem = {
@@ -14,8 +13,10 @@ const Newsitem = {
? m('a.cover', {
href: '/article/' + vnode.attrs.path,
}, m('img', { alt: 'Image for news item ' + vnode.attrs.name, src: pixelRatio > 1 ? vnode.attrs.media.medium_url : vnode.attrs.media.small_url }))
- : m('a.cover.nobg'),
- m('div.entrycontent', [
+ : null,
+ m('div.entrycontent', {
+ class: vnode.attrs.media ? '' : 'extrapadding',
+ }, [
(vnode.attrs.description
? m('.fr-view', m.trust(vnode.attrs.description))
: null),
diff --git a/app/widgets/pages.js b/app/widgets/pages.js
index f46733b..e6471ac 100644
--- a/app/widgets/pages.js
+++ b/app/widgets/pages.js
@@ -1,5 +1,3 @@
-const m = require('mithril')
-
const Pages = {
oninit: function(vnode) {
this.onpage = vnode.attrs.onpage || function() {}
diff --git a/config/config.default.json b/config/config.default.json
index a4d31c4..8e8e998 100644
--- a/config/config.default.json
+++ b/config/config.default.json
@@ -4,6 +4,7 @@
"port": 4030,
"host": "0.0.0.0"
},
+ "CIRCLECI_VERSION": "circleci_version_number",
"knex": {
"client": "pg",
"connection": {
diff --git a/config/config.default.json.org b/config/config.default.json.org
new file mode 100644
index 0000000..8e8e998
--- /dev/null
+++ b/config/config.default.json.org
@@ -0,0 +1,49 @@
+{
+ "NODE_ENV": "development",
+ "server": {
+ "port": 4030,
+ "host": "0.0.0.0"
+ },
+ "CIRCLECI_VERSION": "circleci_version_number",
+ "knex": {
+ "client": "pg",
+ "connection": {
+ "host" : "127.0.0.1",
+ "user" : "postgres",
+ "password" : "postgres",
+ "database" : "nfpmoe"
+ },
+ "connectionslave": null,
+ "migrations": {
+ },
+ "acquireConnectionTimeout": 10000
+ },
+ "bunyan": {
+ "name": "nfpmoe",
+ "streams": [{
+ "stream": "process.stdout",
+ "level": "debug"
+ }
+ ]
+ },
+ "frontend": {
+ "url": "http://localhost:8080"
+ },
+ "jwt": {
+ "secret": "this-is-my-secret",
+ "options": {
+ "expiresIn": 604800
+ }
+ },
+ "googleid": "1076074914074-3no1difo1jq3dfug3glfb25pn1t8idud.apps.googleusercontent.com",
+ "sessionsecret": "this-is-session-secret-lol",
+ "bcrypt": 5,
+ "fileSize": 524288000,
+ "upload": {
+ "baseurl": "http://192.168.42.14",
+ "port": "2111",
+ "host": "storage01.nfp.is",
+ "name": "nfpmoe-dev",
+ "secret": "nfpmoe-dev"
+ }
+}
diff --git a/package.json b/package.json
index 42c5d0d..de481a4 100644
--- a/package.json
+++ b/package.json
@@ -19,13 +19,15 @@
"scripts": {
"lint": "eslint .",
"start": "node --experimental-modules index.mjs",
- "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": "sass -s compressed app/app.scss public/assets/app.css && sass -s compressed app/admin.scss public/assets/admin.css && browserify -p tinyify --no-commondir -o public/assets/app.js app/index.js && browserify -p tinyify --no-commondir -o public/assets/admin.js app/admin.js",
"build:check": "browserify -o public/assets/app.js app/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"watch:api": "nodemon --experimental-modules index.mjs | bunyan",
- "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",
- "dev": "run-p watch:api watch:app watch:sass",
+ "watch:app:admin": "watchify -g envify -d app/admin.js -o public/assets/admin.js",
+ "watch:app:public": "watchify -g envify -d app/index.js -o public/assets/app.js",
+ "watch:sass:public": "sass --watch app/app.scss public/assets/app.css",
+ "watch:sass:admin": "sass --watch app/admin.scss public/assets/admin.css",
+ "dev": "run-p watch:api watch:app:public watch:app:admin watch:sass:public watch:sass:admin",
"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",
@@ -47,6 +49,7 @@
"bcrypt": "^3.0.0",
"bookshelf": "^0.15.1",
"bunyan-lite": "^1.0.1",
+ "dot": "^1.1.2",
"format-link-header": "^2.1.0",
"googleapis": "^42.0.0",
"http-errors": "^1.7.2",
diff --git a/public/assets/admin.css b/public/assets/admin.css
new file mode 100644
index 0000000..fb8d39b
--- /dev/null
+++ b/public/assets/admin.css
@@ -0,0 +1 @@
+.error{font-size:.8em;color:#bb4d00;font-weight:bold;padding-bottom:20px}.admin-wrapper table{width:calc(100% - 20px);margin:10px;border:solid 1px #01579b;border-collapse:collapse;border-spacing:0;font-size:.8em}.admin-wrapper table thead th{background-color:#3d77c7;border:solid 1px #01579b;color:#fff;padding:10px;text-align:left}.admin-wrapper table tbody td{text-align:left;border:solid 1px #01579b;color:#333;padding:10px}.admin-wrapper table a,.admin-wrapper table a:visited,.admin-wrapper table a:hover{text-decoration:none;color:#bb4d00;font-weight:bold}.admin-wrapper table button{color:#bb4d00;background:transparent;border:1px solid #bb4d00}.admin-wrapper table td.right,.admin-wrapper table th.right{text-align:right}.floating-container{position:fixed;top:0;bottom:0;left:0;right:0;background:#00000099;display:flex;flex-direction:column;justify-content:center;align-items:center}.admin-wrapper{flex-grow:2;display:flex;flex-direction:column;background:#01579b;padding:0 20px 50px}.admin-actions{background:#01579b;display:flex;justify-content:center;min-height:37px}.admin-actions span{color:#fff;padding:10px;font-size:14px;font-weight:bold}.admin-actions a{padding:10px;text-decoration:none;color:#ffad42;font-size:14px;font-weight:bold}.fr-box,.fr-toolbar,.fr-box .second-toolbar{border-radius:0 !important}article.editpage{text-align:center;background:#fff;padding:0 0 20px}article.editpage header{padding:10px;background:#f57c00}article.editpage header h1{color:#000}article.editpage header a{font-size:14px;text-decoration:none;margin-left:10px;color:#000}article.editpage fileupload{margin:0 0 20px}article.editpage fileupload .inside{height:150px}article.editpage fileupload.cover{align-self:center;min-width:178px}article.editpage fileupload.cover .display{background-size:auto 100%}article.editpage form{padding:0 40px 20px}article.editpage form textarea{height:300px}article.editpage form .loading-spinner{height:300px;position:relative}article.editpage h5{margin-bottom:20px}article.editpage>.loading-spinner{width:240px;height:50px;position:relative}table span.subpage{padding:0 5px}article.editarticle{text-align:center;background:#fff;padding:0 0 20px}article.editarticle header{padding:10px;background:#f57c00}article.editarticle header h1{color:#000}article.editarticle header a{font-size:14px;text-decoration:none;margin-left:10px;color:#000}article.editarticle fileupload{margin:0 0 20px}article.editarticle fileupload .inside{height:150px}article.editarticle fileupload.cover{align-self:center;min-width:178px}article.editarticle form{padding:0 40px 20px}article.editarticle form textarea{height:300px}article.editarticle form .loading-spinner{height:300px;position:relative}article.editarticle h5{margin-bottom:20px}article.editarticle>.loading-spinner{width:240px;height:50px;position:relative}article.editarticle>.loading-spinner.full{width:100%}article.editarticle .fileupload{align-self:center;padding:.5em;margin:.5em 0;min-width:250px;border:none;border:1px solid #f57c00;background:#ffad42;color:#000;position:relative}article.editarticle .fileupload input{position:absolute;top:0;left:0;right:0;bottom:0;opacity:.01;width:100%;cursor:pointer;text-indent:-9999px;z-index:2}article.editarticle files{align-items:stretch;width:100%;display:flex;flex-direction:column;padding:10px 40px 0;text-align:left}article.editarticle files h4{font-size:1.1em;font-weight:bold;padding:0 5px 5px;margin-bottom:10px;border-bottom:1px solid #ccc}table span.subarticle{padding:0 5px}article.editstaff{text-align:center;background:#fff;padding:0 0 20px}article.editstaff header{padding:10px;background:#f57c00}article.editstaff header h1{color:#000}article.editstaff header a{font-size:14px;text-decoration:none;margin-left:10px;color:#000}article.editstaff form{padding:0 40px 20px}article.editstaff form textarea{height:300px}article.editstaff form .loading-spinner{height:300px;position:relative}article.editstaff h5{margin-bottom:20px}article.editstaff>.loading-spinner{width:240px;height:50px;position:relative}article.editstaff>.loading-spinner.full{width:100%}fileupload{position:relative;display:flex;align-items:stretch;flex-direction:column;justify-content:stretch}fileupload .showicon,fileupload .showbordericon,fileupload .display{flex-grow:2}fileupload .showbordericon{border:3px solid #555;border-style:dashed;background-image:url("./img/upload.svg");background-position:center;background-repeat:no-repeat;background-size:50px}fileupload .showicon{position:absolute;top:5px;right:5px;width:50px;height:50px;background-image:url("./img/upload.svg");background-position:center;background-repeat:no-repeat;background-size:contain}fileupload img{max-width:600px;width:100%;align-self:center;min-height:100px}fileupload .display{background-size:cover;background-repeat:no-repeat;background-position:center}fileupload .loading-spinner{position:absolute;top:0;left:0;right:0;bottom:0;background:#33333388;width:100%}fileupload input{position:absolute;top:0;left:0;right:0;bottom:0;opacity:.01;width:100%;cursor:pointer;text-indent:-9999px;z-index:2}fileupload .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{background:#fff;display:flex;flex-direction:column;text-align:center;width:calc(100% - 40px);max-width:500px;color:#000}dialogue h2{background:#bb4d00;color:#fff;font-size:1.5em;padding:10px}dialogue p{padding:10px}dialogue .buttons{display:flex;justify-content:space-around;padding:10px}dialogue button{border:1px solid #bb4d00;background:transparent;color:#bb4d00;padding:5px 15px;min-width:150px}dialogue button.alert{border-color:red;color:red}dialogue button.cancel{border-color:#999;color:#999}.darkmodeon .maincontainer .admin-wrapper{color:#000}.darkmodeon .error{color:#e05e00}/*# sourceMappingURL=admin.css.map */
diff --git a/public/assets/admin.css.map b/public/assets/admin.css.map
new file mode 100644
index 0000000..0fb703b
--- /dev/null
+++ b/public/assets/admin.css.map
@@ -0,0 +1 @@
+{"version":3,"sourceRoot":"","sources":["../../app/admin.scss","../../app/_common.scss","../../app/admin/admin.scss","../../app/admin/pages.scss","../../app/admin/articles.scss","../../app/admin/staff.scss","../../app/widgets/admin.scss"],"names":[],"mappings":"AAEA,OACE,eACA,MCSkB,QDRlB,iBACA,oBAOF,qBACE,wBACA,YACA,yBACA,yBACA,iBACA,eAEA,8BACE,iBClBe,QDmBf,yBACA,MCnBe,KDoBf,aACA,gBAEF,8BACE,gBACA,yBACA,MCfO,KDgBP,aAEF,mFAGE,qBACA,MCzBgB,QD0BhB,iBAGF,4BACE,MC9BgB,QD+BhB,uBACA,yBAGF,4DAEE,iBAIJ,oBACE,eACA,MACA,SACA,OACA,QACA,qBACA,aACA,sBACA,uBACA,mBE/DF,eACE,YACA,aACA,sBACA,WDJW,QCKX,oBAGF,eACE,WDTW,QCUX,aACA,uBACA,gBAEA,oBACE,MDdS,KCeT,aACA,eACA,iBAGF,iBACE,aACA,qBACA,MDdiB,QCejB,eACA,iBAIJ,4CAGE,2BCjCF,iBACE,kBACA,gBACA,iBAEA,wBACE,aACA,WFCW,QECX,2BACE,MFDS,KEIX,0BACE,eACA,qBACA,iBACA,MFNe,KEUnB,4BACE,gBAEA,oCACE,aAIJ,kCACE,kBACA,gBAEA,2CACE,0BAIJ,sBACE,oBAEA,+BACE,aAGF,uCACE,aACA,kBAIJ,oBACE,mBAGF,kCACE,YACA,YACA,kBAIJ,mBACE,cC/DF,oBACE,kBACA,gBACA,iBAEA,2BACE,aACA,WHCW,QGCX,8BACE,MHDS,KGIX,6BACE,eACA,qBACA,iBACA,MHNe,KGUnB,+BACE,gBAEA,uCACE,aAIJ,qCACE,kBACA,gBAGF,yBACE,oBAEA,kCACE,aAGF,0CACE,aACA,kBAIJ,uBACE,mBAGF,qCACE,YACA,YACA,kBAEA,0CACE,WAIJ,gCACE,kBACA,aACA,cACA,gBACA,YACA,yBACA,WH1DiB,QG2DjB,MH1DiB,KG2DjB,kBAEA,sCACE,kBACA,MACA,OACA,QACA,SACA,YACA,WACA,eACA,oBACA,UAIJ,0BACE,oBACA,WACA,aACA,sBACA,oBACA,gBAEA,6BACE,gBACA,iBACA,kBACA,mBACA,6BAKN,sBACE,cCzGF,kBACE,kBACA,gBACA,iBAEA,yBACE,aACA,WJCW,QICX,4BACE,MJDS,KIIX,2BACE,eACA,qBACA,iBACA,MJNe,KIUnB,uBACE,oBAEA,gCACE,aAGF,wCACE,aACA,kBAIJ,qBACE,mBAGF,mCACE,YACA,YACA,kBAEA,wCACE,WC5CN,WACE,kBACA,aACA,oBACA,sBACA,wBAEA,oEAGE,YAGF,2BACE,sBACA,oBACA,yCACA,2BACA,4BACA,qBAGF,qBACE,kBACA,QACA,UACA,WACA,YACA,yCACA,2BACA,4BACA,wBAGF,eACE,gBACA,WACA,kBACA,iBAGF,oBACE,sBACA,4BACA,2BAGF,4BACE,kBACA,MACA,OACA,QACA,SACA,qBACA,WAGF,iBACE,kBACA,MACA,OACA,QACA,SACA,YACA,WACA,eACA,oBACA,UAGF,mBACE,YACA,kBACA,QACA,WACA,WACA,YACA,yCACA,2BACA,4BACA,6BACA,wBACA,UACA,aACA,eAIJ,SACE,gBACA,aACA,sBACA,kBACA,wBACA,gBACA,MLvEQ,KKyER,YACE,WLtFgB,QKuFhB,MLtFgB,KKuFhB,gBACA,aAGF,WACE,aAGF,kBACE,aACA,6BACA,aAGF,gBACE,yBACA,uBACA,MLzGgB,QK0GhB,iBACA,gBAGF,sBACE,iBACA,UAGF,uBACE,kBACA,WN3DF,0CACE,MC/CM,KDkDR,mBACE,MC7BqB","file":"admin.css"}
\ No newline at end of file
diff --git a/public/assets/admin.js b/public/assets/admin.js
new file mode 100644
index 0000000..cf9032c
--- /dev/null
+++ b/public/assets/admin.js
@@ -0,0 +1 @@
+!function(){var t={};const e={currentUser:null,isAdmin:!1,loadedGoogle:!1,loadingGoogle:!1,loadingListeners:[],authListeners:[],updateToken:function(t){if(!t)return e.clearToken();localStorage.setItem("logintoken",t),e.currentUser=JSON.parse(atob(t.split(".")[1])),e.authListeners.length&&e.authListeners.forEach(function(t){t(e.currentUser)})},clearToken:function(){e.currentUser=null,localStorage.removeItem("logintoken"),e.isAdmin=!1},addEvent:function(t){e.authListeners.push(t)},setAdmin:function(t){e.isAdmin=t},createGoogleScript:function(){return e.loadedGoogle?Promise.resolve():new Promise(function(t){if(e.loadedGoogle)return t();if(e.loadingListeners.push(t),e.loadingGoogle)return;e.loadingGoogle=!0;let a=document.createElement("script");a.type="text/javascript",a.async=!0,a.defer=!0,a.src="https://apis.google.com/js/platform.js?onload=googleLoaded",document.body.appendChild(a)})},getToken:function(){return localStorage.getItem("logintoken")}};window.googleLoaded||(window.googleLoaded=function(){for(e.loadedGoogle=!0;e.loadingListeners.length;)e.loadingListeners.pop()()}),e.updateToken(localStorage.getItem("logintoken")),t=e;var a={sendRequest:function(e,a){let i=t.getToken(),n=a;return i&&(e.headers=e.headers||{},e.headers.Authorization="Bearer "+i),e.extract=function(t){let e=null;if(n&&t.status<300){let a={};t.getAllResponseHeaders().split("\r\n").forEach(function(t){var e=t.split(": ");a[e[0]]=e[1]}),e={headers:a||{},data:JSON.parse(t.responseText)}}else e=t.responseText?JSON.parse(t.responseText):{};if(t.status>=300)throw e;return e},m.request(e).catch(function(e){return 403===e.code&&(t.clearToken(),m.route.set("/login",{redirect:m.route.get()})),e.response&&e.response.status?Promise.reject(e.response):Promise.reject(e)})}},i={};const{sendRequest:n}=a;i.uploadMedia=function(t){let e=new FormData;return e.append("file",t),n({method:"POST",url:"/api/media",body:e})};const{uploadMedia:r}=i;var s={uploadFile(t,e){e.target.files[0]&&(t.state.updateError(t,""),t.state.loading=!0,r(e.target.files[0]).then(function(e){t.attrs.onupload&&t.attrs.onupload(e)}).catch(function(e){t.state.updateError(t,e.message)}).then(function(){e.target.value=null,t.state.loading=!1,m.redraw()}))},updateError:function(t,e){t.attrs.onerror?t.attrs.onerror(e):t.state.error=e},oninit:function(t){t.state.loading=!1,t.state.error=""},view:function(t){let e=t.attrs.media;return m("fileupload",{class:t.attrs.class||null},[m("div.error",{hidden:!t.state.error},t.state.error),e?t.attrs.useimg?[m("img",{src:e.large_url}),m("div.showicon")]:m("a.display.inside",{href:e.large_url,style:{"background-image":'url("'+e.large_url+'")'}},m("div.showicon")):m("div.inside.showbordericon"),m("input",{accept:"image/*",type:"file",onchange:this.uploadFile.bind(this,t)}),e&&t.attrs.ondelete?m("button.remove",{onclick:t.attrs.ondelete}):null,t.state.loading?m("div.loading-spinner"):null])}};const o={files:[{type:"css",url:"https://cdn.jsdelivr.net/npm/froala-editor@3.0.4/css/froala_editor.pkgd.min.css"},{type:"css",url:"https://cdn.jsdelivr.net/npm/froala-editor@3.0.4/css/themes/gray.min.css"},{type:"js",url:"https://cdn.jsdelivr.net/npm/froala-editor@3.0.4/js/froala_editor.pkgd.min.js"}],loadedFiles:0,loadedFroala:!1,checkLoadedAll:function(t){o.loadedFiles ]+data-f-id="pbf"[^>]+>[^>]+>[^>]+>[^>]+>/,"")),this.loading=!0,(a=this.article.id?bt(this.article.id,{name:this.article.name,path:this.article.path,parent_id:this.article.parent_id,description:this.article.description,banner_id:this.article.banner&&this.article.banner.id,media_id:this.article.media&&this.article.media.id}):vt({name:this.article.name,path:this.article.path,parent_id:this.article.parent_id,description:this.article.description,banner_id:this.article.banner&&this.article.banner.id,media_id:this.article.media&&this.article.media.id})).then(function(e){t.state.article.id?(e.media=t.state.article.media,e.banner=t.state.article.banner,e.files=t.state.article.files,t.state.article=e):m.route.set("/admin/articles/"+e.id)}).catch(function(e){t.state.error=e.message}).then(function(){t.state.loading=!1,m.redraw()})},uploadFile(t,e){e.target.files[0]&&(t.state.error="",t.state.loadingFile=!0,gt(this.article.id,e.target.files[0]).then(function(e){t.state.article.files.push(e)}).catch(function(e){t.state.error=e.message}).then(function(){e.target.value=null,t.state.loadingFile=!1,m.redraw()}))},getFlatTree:function(){let t=[{id:null,name:"-- Frontpage --"}];return pt.forEach(function(e){t.push({id:e.id,name:e.name}),e.children.length&&e.children.forEach(function(a){t.push({id:a.id,name:e.name+" -> "+a.name})})}),t},view:function(t){const e=this.getFlatTree();return this.loading?m("div.loading-spinner"):m("div.admin-wrapper",[m("div.admin-actions",this.article.id?[m("span","Actions:"),m(m.route.Link,{href:"/article/"+this.article.path},"View article")]:null),m("article.editarticle",[m("header",m("h1",this.creating?"Create Article":"Edit "+(this.article.name||"(untitled)"))),m("div.error",{hidden:!this.error,onclick:function(){t.state.error=""}},this.error),m(s,{onupload:this.mediaUploaded.bind(this,"banner"),onerror:function(e){t.state.error=e},ondelete:this.mediaRemoved.bind(this,"banner"),media:this.article&&this.article.banner}),m(s,{class:"cover",useimg:!0,onupload:this.mediaUploaded.bind(this,"media"),ondelete:this.mediaRemoved.bind(this,"media"),onerror:function(e){t.state.error=e},media:this.article&&this.article.media}),m("form.editarticle.content",{onsubmit:this.save.bind(this,t)},[m("label","Parent"),m("select",{onchange:this.updateParent.bind(this)},e.map(function(e){return m("option",{value:e.id||-1,selected:e.id===t.state.article.parent_id},e.name)})),m("label","Name"),m("input",{type:"text",value:this.article.name,oninput:this.updateValue.bind(this,"name")}),m("label","Description"),this.loadedFroala?m("div",{oncreate:function(e){t.state.froala=new FroalaEditor(e.dom,wt.getFroalaOptions(),function(){t.state.froala.html.set(t.state.article.description)})}}):null,m("label","Path"),m("input",{type:"text",value:this.article.path,oninput:this.updateValue.bind(this,"path")}),m("div.loading-spinner",{hidden:this.loadedFroala}),m("input",{type:"submit",value:"Save"})]),this.article.files.length?m("files",[m("h4","Files"),this.article.files.map(function(t){return m(ft,{file:t})})]):null,this.article.id?m("div.fileupload",["Add file",m("input",{accept:"*",type:"file",onchange:this.uploadFile.bind(this,t)}),t.state.loadingFile?m("div.loading-spinner"):null]):null])])}};var kt=wt,At={};const{sendRequest:Pt}=a;At.createStaff=function(t){return Pt({method:"POST",url:"/api/staff",body:t})},At.updateStaff=function(t,e){return Pt({method:"PUT",url:"/api/staff/"+t,body:e})},At.getAllStaff=function(){return Pt({method:"GET",url:"/api/staff"})},At.getStaff=function(t){return Pt({method:"GET",url:"/api/staff/"+t})},At.removeStaff=function(t){return Pt({method:"DELETE",url:"/api/staff/"+t})};const{getAllStaff:xt,removeStaff:St}=At,Ft={oninit:function(t){this.error="",this.lastpage=m.route.param("page")||"1",this.staff=[],this.removeStaff=null,this.fetchStaffs(t)},fetchStaffs:function(t){return this.loading=!0,xt().then(function(e){t.state.staff=e}).catch(function(e){t.state.error=e.message}).then(function(){t.state.loading=!1,m.redraw()})},confirmRemoveStaff:function(t){let e=this.removeStaff;this.removeStaff=null,this.loading=!0,St(e.id).then(this.oninit.bind(this,t)).catch(function(e){t.state.error=e.message,t.state.loading=!1,m.redraw()})},getLevel:function(t){return 100===t?"Admin":"Manager"},view:function(t){return[m("div.admin-wrapper",[m("div.admin-actions",[m("span","Actions:"),m(m.route.Link,{href:"/admin/staff/add"},"Create new staff")]),m("article.editarticle",[m("header",m("h1","All staff")),m("div.error",{hidden:!this.error,onclick:function(){t.state.error=""}},this.error),this.loading?m("div.loading-spinner.full"):m("table",[m("thead",m("tr",[m("th","Fullname"),m("th","Email"),m("th","Level"),m("th.right","Updated"),m("th.right","Actions")])),m("tbody",this.staff.map(function(e){return m("tr",[m("td",m(m.route.Link,{href:"/admin/staff/"+e.id},e.fullname)),m("td",e.email),m("td.right",Ft.getLevel(e.level)),m("td.right",(e.updated_at||"---").replace("T"," ").split(".")[0]),m("td.right",m("button",{onclick:function(){t.state.removeStaff=e}},"Remove"))])}))]),m(rt,{base:"/admin/staff",links:this.links})])]),m(y,{hidden:null===t.state.removeStaff,title:"Delete "+(t.state.removeStaff?t.state.removeStaff.name:""),message:'Are you sure you want to remove "'+(t.state.removeStaff?t.state.removeStaff.fullname:"")+'" ('+(t.state.removeStaff?t.state.removeStaff.email:"")+")",yes:"Remove",yesclass:"alert",no:"Cancel",noclass:"cancel",onyes:this.confirmRemoveStaff.bind(this,t),onno:function(){t.state.removeStaff=null}})]}};var Lt=Ft;const{createStaff:Tt,updateStaff:Ot,getStaff:jt}=At;var _t={oninit:function(t){this.fetchStaff(t)},onupdate:function(t){this.lastid!==m.route.param("id")&&this.fetchStaff(t)},fetchStaff:function(t){this.lastid=m.route.param("id"),this.loading="add"!==this.lastid,this.creating="add"===this.lastid,this.error="",this.staff={fullname:"",email:"",password:"",level:10},"add"!==this.lastid&&jt(this.lastid).then(function(e){t.state.editedPath=!0,t.state.staff=e}).catch(function(e){t.state.error=e.message}).then(function(){t.state.loading=!1,m.redraw()})},updateValue:function(t,e){this.staff[t]=e.currentTarget.value},save:function(t,e){if(e.preventDefault(),this.staff.fullname?this.staff.email?this.error="":this.error="Email is missing":this.error="Fullname is missing",this.error)return;let a;this.staff.description=t.state.froala&&t.state.froala.html.get()||this.staff.description,this.loading=!0,(a=this.staff.id?Ot(this.staff.id,{fullname:this.staff.fullname,email:this.staff.email,level:this.staff.level,password:this.staff.password}):Tt({fullname:this.staff.fullname,email:this.staff.email,level:this.staff.level,password:this.staff.password})).then(function(t){m.route.set("/admin/staff")}).catch(function(e){t.state.error=e.message}).then(function(){t.state.loading=!1,m.redraw()})},updateLevel:function(t){this.staff.level=Number(t.currentTarget.value)},view:function(t){return this.loading?m("div.loading-spinner"):m("div.admin-wrapper",[m("div.admin-actions",this.staff.id?[m("span","Actions:"),m(m.route.Link,{href:"/admin/staff"},"Staff list")]:null),m("article.editstaff",[m("header",m("h1",this.creating?"Create Staff":"Edit "+(this.staff.fullname||"(untitled)"))),m("div.error",{hidden:!this.error,onclick:function(){t.state.error=""}},this.error),m("form.editstaff.content",{onsubmit:this.save.bind(this,t)},[m("label","Level"),m("select",{onchange:this.updateLevel.bind(this)},[[10,"Manager"],[100,"Admin"]].map(function(e){return m("option",{value:e[0],selected:e[0]===t.state.staff.level},e[1])})),m("label","Fullname"),m("input",{type:"text",value:this.staff.fullname,oninput:this.updateValue.bind(this,"fullname")}),m("label","Email"),m("input",{type:"text",value:this.staff.email,oninput:this.updateValue.bind(this,"email")}),m("label","Password (optional)"),m("input",{type:"text",value:this.staff.password,oninput:this.updateValue.bind(this,"password")}),m("input",{type:"submit",value:"Save"})])])])}};window.addAdminRoutes=[["/admin/pages",P],["/admin/pages/:key",b],["/admin/articles",dt],["/admin/articles/:id",kt],["/admin/staff",Lt],["/admin/staff/:id",_t]]}();
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index a481ad0..92b521b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -6,20 +6,22 @@