diff --git a/base/article/routes.mjs b/base/article/routes.mjs index 49c44c8..9d1afea 100644 --- a/base/article/routes.mjs +++ b/base/article/routes.mjs @@ -1,13 +1,12 @@ -import { FormidableHandler } from 'flaska' -import { parseFiles } from '../file/util.mjs' import { parseArticles, parseArticle } from './util.mjs' -import { upload } from '../media/upload.mjs' +import { uploadMedia, uploadFile } from '../media/upload.mjs' import { mediaToDatabase } from '../media/util.mjs' export default class ArticleRoutes { constructor(opts = {}) { Object.assign(this, { - upload: upload, + uploadMedia: uploadMedia, + uploadFile: uploadFile, }) } @@ -26,12 +25,13 @@ export default class ArticleRoutes { async getArticle(ctx) { let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path]) - let out = { + ctx.body = this.getArticle_resOutput(res) + } + + getArticle_resOutput(res) { + return { article: parseArticle(res.results[0][0]), - files: parseFiles(res.results[1]), } - - ctx.body = out } /** GET: /api/auth/articles */ @@ -72,16 +72,16 @@ export default class ArticleRoutes { console.log(params) let res = await ctx.db.safeCallProc('article_auth_get_update_create', params) - let out = { - article: parseArticle(res.results[0][0]) || { publish_at: new Date() }, - files: parseFiles(res.results[1]), - staff: res.results[2], - } - - ctx.body = out + ctx.body = this.private_getUpdateArticle_resOutput(res) + } + + private_getUpdateArticle_resOutput(res) { + return { + article: parseArticle(res.results[0][0] || {}), + staff: res.results[1], + } } - /** GET: /api/auth/articles/:id */ auth_getSingleArticle(ctx) { return this.private_getUpdateArticle(ctx) @@ -98,13 +98,13 @@ export default class ArticleRoutes { if (ctx.req.files.banner) { promises.push( - this.upload(ctx.req.files.banner) + this.uploadMedia(ctx.req.files.banner) .then(res => { newBanner = res }) ) } if (ctx.req.files.media) { promises.push( - this.upload(ctx.req.files.media) + this.uploadMedia(ctx.req.files.media) .then(res => { newMedia = res }) ) } diff --git a/base/article/util.mjs b/base/article/util.mjs index 20bbe62..45d82c9 100644 --- a/base/article/util.mjs +++ b/base/article/util.mjs @@ -1,4 +1,3 @@ -import { parseFile } from '../file/util.mjs' import { contentToBlocks, parseMediaAndBanner } from '../util.mjs' export function parseArticles(articles) { @@ -8,18 +7,6 @@ export function parseArticles(articles) { return articles } -export function combineFilesWithArticles(articles, files) { - let articleMap = new Map() - - articles.forEach(article => { - article.files = [] - articleMap.set(article.id, article) - }) - files.forEach(file => { - articleMap.get(file.id).files.push(parseFile(file)) - }) -} - export function parseArticle(article) { if (!article) { return null diff --git a/base/config.mjs b/base/config.mjs index 589d4ea..351ab42 100644 --- a/base/config.mjs +++ b/base/config.mjs @@ -66,6 +66,7 @@ nconf.defaults({ "secret": "upload-secret-key-here", "iss": "dev", "path": "https://media.nfp.is/media/resize", + "filePath": "https://media.nfp.is/media", "preview": { "out": "base64", "format": "avif", diff --git a/base/media/upload.mjs b/base/media/upload.mjs index 577be0b..6fe8537 100644 --- a/base/media/upload.mjs +++ b/base/media/upload.mjs @@ -1,7 +1,7 @@ import config from '../config.mjs' import Client from './client.mjs' -export function upload(file) { +export function uploadMedia(file) { const media = config.get('media') const client = new Client() @@ -52,4 +52,23 @@ export function upload(file) { .then(() => { return out }) +} + +export function uploadFile(file) { + const media = config.get('media') + + const client = new Client() + let token = client.createJwt({ iss: media.iss }, media.secret) + + return client.upload(media.filePath + '?token=' + token, { file: { + file: file.path, + filename: file.name, + } }, 'POST').then(res => { + return { + filename: res.filename, + path: res.path, + size: file.size, + type: file.type, + } + }) } \ No newline at end of file diff --git a/base/package.json b/base/package.json index b4675a4..f74019a 100644 --- a/base/package.json +++ b/base/package.json @@ -1,7 +1,7 @@ { "dependencies": { "dot": "^2.0.0-beta.1", - "flaska": "^1.3.0", + "flaska": "^1.3.1", "formidable": "^1.2.6", "msnodesqlv8": "^2.4.7", "nconf-lite": "^1.0.1" diff --git a/base/page/routes.mjs b/base/page/routes.mjs index 78a05fa..c355ec0 100644 --- a/base/page/routes.mjs +++ b/base/page/routes.mjs @@ -1,13 +1,14 @@ import { parsePage, parsePagesToTree } from './util.mjs' -import { upload } from '../media/upload.mjs' -import { combineFilesWithArticles, parseArticle, parseArticles } from '../article/util.mjs' +import { uploadMedia, uploadFile } from '../media/upload.mjs' +import { parseArticle, parseArticles } from '../article/util.mjs' import { mediaToDatabase } from '../media/util.mjs' export default class PageRoutes { constructor(opts = {}) { Object.assign(this, { - upload: upload, + uploadMedia: uploadMedia, + uploadFile: uploadFile, }) } @@ -42,16 +43,16 @@ export default class PageRoutes { Math.min(ctx.query.get('per_page') || 10, 25), ]) - let out = { + ctx.body = this.getPage_resOut(res) + } + + getPage_resOut(res) { + return { page: parsePage(res.results[0][0]), articles: parseArticles(res.results[1]), total_articles: res.results[2][0].total_articles, featured: parseArticle(res.results[4][0]), } - - combineFilesWithArticles(out.articles, res.results[3]) - - ctx.body = out } /** GET: /api/auth/pages */ @@ -106,13 +107,13 @@ export default class PageRoutes { if (ctx.req.files.banner) { promises.push( - this.upload(ctx.req.files.banner) + this.uploadMedia(ctx.req.files.banner) .then(res => { newBanner = res }) ) } if (ctx.req.files.media) { promises.push( - this.upload(ctx.req.files.media) + this.uploadMedia(ctx.req.files.media) .then(res => { newMedia = res }) ) } diff --git a/base/server.mjs b/base/server.mjs index 3984c78..f8f691e 100644 --- a/base/server.mjs +++ b/base/server.mjs @@ -26,29 +26,17 @@ export default class Server { this.authenticate = authenticate this.formidable = FormidableHandler.bind(this, formidable) this.jsonHandler = JsonHandler - this.routes = [ - new PageRoutes(), - new ArticleRoutes(), - new AuthenticationRoutes(), - ] + this.routes = { + page: new PageRoutes(), + article: new ArticleRoutes(), + auth: new AuthenticationRoutes(), + } this.init() } init() { } - getRouteInstance(type) { - for (let route of this.routes) { - if (route instanceof type) { - return route - } - } - } - - addCustomRoutes() { - - } - runCreateServer() { // Create our server this.flaska = new Flaska(this.flaskaOptions, this.http) @@ -89,8 +77,9 @@ export default class Server { } runRegisterRoutes() { - for (let route of this.routes) { - route.register(this) + let keys = Object.keys(this.routes) + for (let key of keys) { + this.routes[key].register(this) } } @@ -105,7 +94,6 @@ export default class Server { } run() { - this.addCustomRoutes() this.runCreateServer() this.runRegisterRoutes() diff --git a/nfp_moe/api/article_routes.mjs b/nfp_moe/api/article_routes.mjs new file mode 100644 index 0000000..7ff5e23 --- /dev/null +++ b/nfp_moe/api/article_routes.mjs @@ -0,0 +1,84 @@ +import { parseArticles, parseArticle } from '../base/article/util.mjs' +import { parseFiles } from './file/util.mjs' +import Parent from '../base/article/routes.mjs' +import { decodeTorrentFile } from './file/torrent.mjs' + +export default class ArticleRoutes extends Parent { + register(server) { + super.register(server) + server.flaska.post('/api/auth/articles/:id/files', [ + server.authenticate(), + server.formidable({ maxFileSize: 100 * 1024 * 1024, }), + ], this.auth_addFileToArticle.bind(this)) + } + + getArticle_resOutput(res) { + return { + article: parseArticle(res.results[0][0]), + files: parseFiles(res.results[1]), + } + } + + private_getUpdateArticle_resOutput(res) { + return { + article: parseArticle(res.results[0][0] || {}), + files: parseFiles(res.results[1]), + staff: res.results[2], + } + } + + /** POST: /api/auth/articles/:id/files */ + async auth_addFileToArticle(ctx) { + if (!ctx.req.files.file) { + throw new HttpError(422, 'Missing file in upload') + } + + let meta = {} + if (ctx.req.files.file.name.endsWith('.torrent')) { + try { + let torrent = await decodeTorrentFile(ctx.req.files.file.path) + meta.torrent = { + name: torrent.name, + announce: torrent.announce, + hash: torrent.infoHash, + files: torrent.files.map(file => ({ name: file.name, size: file.length })), + } + } catch (err) { + ctx.log.error(err) + } + } + let file = await this.uploadFile(ctx.req.files.file) + + let params = [ + ctx.state.auth_token, + ctx.params.id, + file.filename, + file.type, + file.path, + file.size, + JSON.stringify(meta), + null, + 0, + ] + + await ctx.db.safeCallProc('article_auth_file_create_delete', params) + } + + /** DELETE: /api/auth/articles/:id/files/:fileId */ + async auth_addFileToArticle(ctx) { + let params = [ + ctx.state.auth_token, + ctx.params.id, + null, + null, + null, + null, + null, + ctx.params.fileId, + 0, + ] + + let res = await ctx.db.safeCallProc('article_auth_file_create_delete', params) + console.log(res) + } +} \ No newline at end of file diff --git a/base/file/torrent.mjs b/nfp_moe/api/file/torrent.mjs similarity index 89% rename from base/file/torrent.mjs rename to nfp_moe/api/file/torrent.mjs index be47192..76c8524 100644 --- a/base/file/torrent.mjs +++ b/nfp_moe/api/file/torrent.mjs @@ -1,3 +1,6 @@ +import fs from 'fs/promises' +import crypto from 'crypto' +import path from 'path' import bencode from 'bencode' /* @@ -9,10 +12,9 @@ Taken from parse-torrent * @param {Buffer|Object} torrent * @return {Object} parsed torrent */ -export function decodeTorrentFile (torrent) { - if (Buffer.isBuffer(torrent)) { - torrent = bencode.decode(torrent) - } +export async function decodeTorrentFile (file) { + let buffer = await fs.readFile(file) + let torrent = bencode.decode(buffer) // sanity check ensure(torrent.info, 'info') @@ -36,7 +38,9 @@ export function decodeTorrentFile (torrent) { announce: [] } - result.infoHash = sha1.sync(result.infoBuffer) + result.infoHash = crypto.createHash('sha1') + .update(result.infoBuffer) + .digest('hex') result.infoHashBuffer = Buffer.from(result.infoHash, 'hex') if (torrent.info.private !== undefined) result.private = !!torrent.info.private @@ -103,3 +107,7 @@ function splitPieces (buf) { function ensure (bool, fieldName) { if (!bool) throw new Error(`Torrent is missing required field: ${fieldName}`) } + +function sumLength (sum, file) { + return sum + file.length +} diff --git a/base/file/util.mjs b/nfp_moe/api/file/util.mjs similarity index 67% rename from base/file/util.mjs rename to nfp_moe/api/file/util.mjs index 37bc67c..b804ed5 100644 --- a/base/file/util.mjs +++ b/nfp_moe/api/file/util.mjs @@ -5,6 +5,18 @@ export function parseFiles(files) { return files } +export function combineFilesWithArticles(articles, files) { + let articleMap = new Map() + + articles.forEach(article => { + article.files = [] + articleMap.set(article.id, article) + }) + files.forEach(file => { + articleMap.get(file.id).files.push(parseFile(file)) + }) +} + export function parseFile(file) { file.url = 'https://cdn.nfp.is' + file.path file.magnet = null diff --git a/nfp_moe/api/page_routes.mjs b/nfp_moe/api/page_routes.mjs new file mode 100644 index 0000000..f80ae41 --- /dev/null +++ b/nfp_moe/api/page_routes.mjs @@ -0,0 +1,19 @@ +import { parseArticles, parseArticle } from '../../base/article/util.mjs' +import Parent from '../../base/page/routes.mjs' +import { parsePage } from '../../base/page/util.mjs' +import { combineFilesWithArticles } from './file/util.mjs' + +export default class PageRoutes extends Parent { + getPage_resOut(res) { + let out = { + page: parsePage(res.results[0][0]), + articles: parseArticles(res.results[1]), + total_articles: res.results[2][0].total_articles, + featured: parseArticle(res.results[4][0]), + } + + combineFilesWithArticles(out.articles, res.results[3]) + + return out + } +} \ No newline at end of file diff --git a/nfp_moe/api/server.mjs b/nfp_moe/api/server.mjs index 754867f..ae393bb 100644 --- a/nfp_moe/api/server.mjs +++ b/nfp_moe/api/server.mjs @@ -1,22 +1,22 @@ import config from '../base/config.mjs' import Parent from '../base/server.mjs' import ServeHandler from '../base/serve.mjs' -import PageRoutes from '../base/page/routes.mjs' +import ArticleRoutes from './article_routes.mjs' +import PageRoutes from './page_routes.mjs' export default class Server extends Parent { init() { - this.flaskaOptions.appendHeaders['Content-Security-Policy'] = `default-src 'self'; script-src 'self' talk.hyvor.com; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-src talk.hyvor.com` //; frame-ancestors 'none'` - } - - addCustomRoutes() { - let page = this.getRouteInstance(PageRoutes) - + super.init() let localUtil = new this.core.sc.Util(import.meta.url) - this.routes.push(new ServeHandler({ - pageRoutes: page, + + this.flaskaOptions.appendHeaders['Content-Security-Policy'] = `default-src 'self'; script-src 'self' talk.hyvor.com; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-src talk.hyvor.com` //; frame-ancestors 'none'` + this.routes.article = new ArticleRoutes() + this.routes.page = new PageRoutes() + this.routes.serve = new ServeHandler({ + pageRoutes: this.routes.page, root: localUtil.getPathFromRoot('../public'), version: this.core.app.running, frontend: config.get('frontend:url'), - })) + }) } } diff --git a/nfp_moe/app/admin/admin.js b/nfp_moe/app/admin/admin.js index 2d5ae8d..a8be9ab 100644 --- a/nfp_moe/app/admin/admin.js +++ b/nfp_moe/app/admin/admin.js @@ -1,7 +1,7 @@ const EditPage = require('./site_editpage') const AllPages = require('./site_pages') const AllArticles = require('./site_articles') -const EditArticle = require('./editarticle') +const EditArticle = require('./site_editarticle') const AllStaff = require('./stafflist') const EditStaff = require('./editstaff') const Dialogue = require('./dialogue') diff --git a/nfp_moe/app/admin/dialogue.js b/nfp_moe/app/admin/dialogue.js index e74cc0d..b2f387d 100644 --- a/nfp_moe/app/admin/dialogue.js +++ b/nfp_moe/app/admin/dialogue.js @@ -29,7 +29,9 @@ const Dialogue = { view: function(vnode) { let data = Dialogue.showDialogueData return data - ? m('div.floating-container.main', m('dialogue', [ + ? m('div.floating-container.main', { + onclick: this.onclose.bind(this), + }, m('dialogue', { onclick: function(e) { e.stopPropagation() } }, [ m('h2.title', data.title), m('p', data.message), m('div.buttons', [ diff --git a/nfp_moe/app/admin/froala.js b/nfp_moe/app/admin/froala.js deleted file mode 100644 index 8fc706b..0000000 --- a/nfp_moe/app/admin/froala.js +++ /dev/null @@ -1,46 +0,0 @@ -const Froala = { - files: [ - { type: 'css', url: 'https://cdn.jsdelivr.net/npm/froala-editor@3.0.4/css/froala_editor.pkgd.min.css' }, - { type: 'css', url: 'https://cdn.jsdelivr.net/npm/froala-editor@3.0.4/css/themes/gray.min.css' }, - { type: 'js', url: 'https://cdn.jsdelivr.net/npm/froala-editor@3.0.4/js/froala_editor.pkgd.min.js' }, - ], - loadedFiles: 0, - loadedFroala: false, - - checkLoadedAll: function(res) { - if (Froala.loadedFiles < Froala.files.length) { - return - } - Froala.loadedFroala = true - res() - }, - - createFroalaScript: function() { - if (Froala.loadedFroala) return Promise.resolve() - return new Promise(function(res) { - let onload = function() { - Froala.loadedFiles++ - Froala.checkLoadedAll(res) - } - let head = document.getElementsByTagName('head')[0] - - for (var i = 0; i < Froala.files.length; i++) { - let element - if (Froala.files[i].type === 'css') { - element = document.createElement('link') - element.setAttribute('rel', 'stylesheet') - element.setAttribute('type', 'text/css') - element.setAttribute('href', Froala.files[i].url) - } else { - element = document.createElement('script') - element.setAttribute('type', 'text/javascript') - element.setAttribute('src', Froala.files[i].url) - } - element.onload = onload - head.insertBefore(element, head.firstChild) - } - }) - }, -} - -module.exports = Froala diff --git a/nfp_moe/app/admin/site_articles.js b/nfp_moe/app/admin/site_articles.js index df71cfc..cbc11d2 100644 --- a/nfp_moe/app/admin/site_articles.js +++ b/nfp_moe/app/admin/site_articles.js @@ -13,7 +13,6 @@ const AdminArticles = { articles: [], total_articles: 0, } - this.removeArticle = null this.currentPage = Number(m.route.param('page')) || 1 this.fetchArticles(vnode) @@ -70,24 +69,34 @@ const AdminArticles = { }) }, - confirmRemoveArticle: function(vnode) { - let removingArticle = this.removeArticle - this.removeArticle = null + confirmRemoveArticle: function(vnode, article) { this.loading = true m.redraw() - return common.sendRequest({ + return api.sendRequest({ method: 'DELETE', - url: '/api/auth/articles/' + removingArticle.id, + url: '/api/auth/articles/' + article.id, }) .then( () => this.fetchArticles(vnode), - (err) => { - this.error = err.message - this.loading = false - m.redraw() - } + (err) => { this.error = err.message } ) + .then(() => { + this.loading = false + m.redraw() + }) + }, + + askConfirmRemovePage: function(vnode, article) { + Dialogue.showDialogue( + 'Delete ' + article.name, + 'Are you sure you want to remove "' + article.name + '" (' + article.path + ')', + 'Remove', + 'alert', + 'Don\'t remove', + '', + article, + this.confirmRemoveArticle.bind(this, vnode)) }, drawArticle: function(vnode, article) { @@ -104,7 +113,7 @@ const AdminArticles = { m('td', m(m.route.Link, { href: article.page_path }, article.page_name)), m('td.right', article.publish_at.replace('T', ' ').split('.')[0]), m('td.right', article.admin_name), - m('td.right', m('button', { onclick: function() { vnode.state.removeArticle = article } }, 'Remove')), + m('td.right', m('button', { onclick: this.askConfirmRemovePage.bind(this, vnode, article) }, 'Remove')), ]), ] }, @@ -113,51 +122,42 @@ const AdminArticles = { return [ m('div.admin', [ m('div.inside.vertical', [ - m('div.spacer'), - m('h2.title', 'All articles'), m('div.actions', [ + m('div.filler'), m('span', 'Actions:'), m(m.route.Link, { href: '/admin/articles/add' }, 'Create new article'), ]), - m('div.error', { - hidden: !this.error, - onclick: function() { vnode.state.error = '' }, - }, this.error), - this.loading - ? m('div.loading-spinner.full') - : m('table', [ - m('thead', - m('tr', [ - m('th', 'Title'), - m('th', 'Path'), - m('th', 'Page'), - m('th.right', 'Publish'), - m('th.right', 'By'), - m('th.right', 'Actions'), - ]) - ), - m('tbody', this.data.articles.map((article) => this.drawArticle(vnode, article))), - ], - ), - m(Paginator, { - base: '/admin/articles', - page: this.currentPage, - perPage: ItemsPerPage, - total: this.data.total_articles, - }), + m('h2.title', 'All articles'), + m('div.container', [ + m('div.error', { + hidden: !this.error, + onclick: function() { vnode.state.error = '' }, + }, this.error), + this.loading + ? m('div.loading-spinner.full') + : m('table', [ + m('thead', + m('tr', [ + m('th', 'Title'), + m('th', 'Path'), + m('th', 'Page'), + m('th.right', 'Publish'), + m('th.right', 'By'), + m('th.right', 'Actions'), + ]) + ), + m('tbody', this.data.articles.map((article) => this.drawArticle(vnode, article))), + ], + ), + m(Paginator, { + base: '/admin/articles', + page: this.currentPage, + perPage: ItemsPerPage, + total: this.data.total_articles, + }), + ]), ]), ]), - m(Dialogue, { - hidden: vnode.state.removeArticle === null, - title: 'Delete ' + (vnode.state.removeArticle ? vnode.state.removeArticle.name : ''), - message: 'Are you sure you want to remove "' + (vnode.state.removeArticle ? vnode.state.removeArticle.name : '') + '" (' + (vnode.state.removeArticle ? vnode.state.removeArticle.path : '') + ')', - yes: 'Remove', - yesclass: 'alert', - no: 'Cancel', - noclass: 'cancel', - onyes: this.confirmRemoveArticle.bind(this, vnode), - onno: function() { vnode.state.removeArticle = null }, - }), ] }, } diff --git a/nfp_moe/app/admin/editarticle.js b/nfp_moe/app/admin/site_editarticle.js similarity index 51% rename from nfp_moe/app/admin/editarticle.js rename to nfp_moe/app/admin/site_editarticle.js index 0e077ae..423d616 100644 --- a/nfp_moe/app/admin/editarticle.js +++ b/nfp_moe/app/admin/site_editarticle.js @@ -17,6 +17,7 @@ const EditArticle = { } this.pages = [{id: null, name: 'Frontpage'}] this.pages = this.pages.concat(PageTree.getFlatTree()) + this.newBanner = null this.newMedia = null this.dateInstance = null @@ -62,12 +63,13 @@ const EditArticle = { data .then((result) => { this.data = result - this.data.article.publish_at = new Date(this.data.article.publish_at) if (this.data.article.id) { + this.data.article.publish_at = new Date(this.data.article.publish_at) document.title = 'Editing: ' + this.data.article.name + ' - Admin NFP Moe' this.editedPath = true } else { + this.data.article.publish_at = new Date('3000-01-01') document.title = 'Create Article - Admin NFP Moe' } }, (err) => { @@ -172,41 +174,249 @@ const EditArticle = { }, uploadFile: function(vnode, e) { + if (!e.target.files[0]) return + if (this.lastid === 'add') return + let file = e.target.files[0] + e.target.value = null + + let formData = new FormData() + formData.append('file', file) + + return this.refreshFiles(api.sendRequest({ + method: 'POST', + url: '/api/auth/articles/' + this.lastid + '/files', + body: formData, + })) + }, + + refreshFiles: function(vnode, prom) { + prom.then(() => { + return api.sendRequest({ + method: 'GET', + url: '/api/auth/articles/' + this.lastid, + }) + }) + .then((result) => { + this.data.files = result.files + }, (err) => { + this.error = err.message + }) + .then(() => { + m.redraw() + }) + }, + + askConfirmRemoveFile: function(vnode, file) { + console.log(file) + /*Dialogue.showDialogue( + 'Delete ' + page.name, + 'Are you sure you want to remove "' + page.name + '" (' + page.path + ')', + 'Remove', + 'alert', + 'Don\'t remove', + '', + page, + this.confirmRemovePage.bind(this, vnode))*/ }, view: function(vnode) { - const showPublish = this.data.article - ? this.data.article.publish_at > new Date() + let article = this.data.article + const showPublish = article + ? article.publish_at > new Date() : false - const bannerImage = this.data.article && this.data.article.banner_prefix - ? this.data.article.banner_prefix + '_large.avif' + console.log(!!article, article && article.publish_at > new Date(),'=', showPublish) + const bannerImage = article && article.banner_alt_prefix + ? article.banner_alt_prefix + '_large.avif' : null - const mediaImage = this.data.article && this.data.article.media_prefix - ? this.data.article.media_prefix + '_large.avif' + const mediaImage = article && article.media_alt_prefix + ? article.media_alt_prefix + '_large.avif' : null return [ - this.loading && !this.data.article + m('div.admin', [ + !this.loading + ? m(FileUpload, { + class: 'banner', + height: 150, + onfile: this.mediaUploaded.bind(this, 'banner'), + ondelete: this.mediaRemoved.bind(this, 'banner'), + media: bannerImage, + }, 'Click to upload banner image (only visible when featured)') + : null, + m('div.inside.vertical', [ + m('div.actions', [ + '« ', + m(m.route.Link, { href: '/admin/articles' }, 'Articles'), + article && article.id + ? [ + m('div.filler'), + m('span', 'Actions:'), + m(m.route.Link, { href: '/article/' + article.path }, 'View article'), + ] + : null, + ]), + m('h2.title', this.lastid === 'add' ? 'Create article' : 'Edit ' + (article && article.name || '(untitled)')), + m('div.container', [ + m('div.error', { + hidden: !this.error, + onclick: function() { vnode.state.error = '' }, + }, this.error), + this.loading + ? m('div.loading-spinner') + : null, + article + ? [ + m(FileUpload, { + class: 'cover', + useimg: true, + onfile: this.mediaUploaded.bind(this, 'media'), + ondelete: this.mediaRemoved.bind(this, 'media'), + media: mediaImage, + }, 'Click to upload article image'), + m('form', { + onsubmit: this.save.bind(this, vnode), + }, [ + m('label', 'Parent'), + m('select', { + onchange: this.updateParent.bind(this), + }, this.pages.map((item) => { + return m('option', { + value: item.id || 0, + selected: item.id === article.page_id + }, item.name) + })), + m('div.input-row', [ + m('div.input-group', [ + m('label', 'Name'), + m('input', { + type: 'text', + value: article.name, + oninput: this.updateValue.bind(this, 'name'), + }), + ]), + m('div.input-group', [ + m('label', 'Path'), + m('input', { + type: 'text', + value: article.path, + oninput: this.updateValue.bind(this, 'path'), + }), + ]), + ]), + m('label', 'Description'), + m(Editor, { + oncreate: (subnode) => { + this.editor = subnode.state.editor + }, + contentdata: article.content, + }), + m('div.input-row', [ + m('div', [ + m('label', 'Published at'), + m('input', { + type: 'text', + oncreate: (div) => { + if (!this.dateInstance) { + this.dateInstance = new dtsel.DTS(div.dom, { + dateFormat: 'yyyy-mm-dd', + timeFormat: 'HH:MM:SS', + showTime: true, + }) + window.temp = this.dateInstance + } + }, + value: article.publish_at.toISOString().replace('T', ', ').split('.')[0], + }), + ]), + m('div', [ + m('label', 'Published by'), + m('select', { + onchange: this.updateStaffer.bind(this), + }, + this.data.staff.map((item) => { + return m('option', { + value: item.id, + selected: item.id === article.admin_id + }, item.name) + }) + ), + ]), + m('div.slim', [ + m('label', 'Make featured'), + m('input', { + type: 'checkbox', + checked: article.is_featured, + oninput: this.updateValue.bind(this, 'is_featured'), + }), + ]), + ]), + m('div.actions', { + hidden: !article.name || !article.path + }, [ + m('input', { + type: 'submit', + value: article.id ? 'Save' : 'Create', + }), + showPublish + ? m('button', { + onclick: () => { + this.dateInstance.inputElem.value = (new Date().toISOString()).replace('T', ', ').split('.')[0] + } + }, 'Publish now') + : null, + ]), + ]), + m('files', [ + m('h5', 'Files'), + this.data.files.map((file) => { + return m( + Fileinfo, + { file: file }, + m('div.remove', + m('button', { onclick: () => this.askConfirmRemoveFile(vnode, file) }, 'remove') + ) + ) + }), + ]), + article.id + ? m('div.actions', [ + m('button.fileupload', [ + 'Add file', + m('input', { + accept: '*', + type: 'file', + onchange: this.uploadFile.bind(this, vnode), + }), + ]) + ]) + : null, + ] + : null, + ]), + ]), + ]), + /* + this.loading && !article ? m('div.admin-spinner.loading-spinner') : null, - this.data.article + article ? m('div.admin-wrapper', [ this.loading ? m('div.loading-spinner') : null, - m('div.admin-actions', this.data.article.id + m('div.admin-actions', article.id ? [ m('span', 'Actions:'), - m(m.route.Link, { href: '/article/' + this.data.article.path }, 'View article'), + m(m.route.Link, { href: '/article/' + article.path }, 'View article'), ] : null), m('article.editarticle', [ m('header', m('h1', - (this.data.article.id ? 'Edit ' : 'Create Article ') + (this.data.article.name || '(untitled)') + (article.id ? 'Edit ' : 'Create Article ') + (article.name || '(untitled)') ) ), - m('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (this.data.article.name || '(untitled)'))), + m('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (article.name || '(untitled)'))), m('div.error', { hidden: !this.error, onclick: () => { vnode.state.error = '' }, @@ -233,7 +443,7 @@ const EditArticle = { }, this.pages.map((item) => { return m('option', { value: item.id || 0, - selected: item.id === this.data.article.page_id + selected: item.id === article.page_id }, item.name) })), m('div.input-row', [ @@ -241,7 +451,7 @@ const EditArticle = { m('label', 'Name'), m('input', { type: 'text', - value: this.data.article.name, + value: article.name, oninput: this.updateValue.bind(this, 'name'), }), ]), @@ -249,7 +459,7 @@ const EditArticle = { m('label', 'Path'), m('input', { type: 'text', - value: this.data.article.path, + value: article.path, oninput: this.updateValue.bind(this, 'path'), }), ]), @@ -259,7 +469,7 @@ const EditArticle = { oncreate: (subnode) => { this.editor = subnode.state.editor }, - contentdata: this.data.article.content, + contentdata: article.content, }), m('div.input-row', [ m('div.input-group', [ @@ -276,7 +486,7 @@ const EditArticle = { window.temp = this.dateInstance } }, - value: this.data.article.publish_at.toISOString().replace('T', ', ').split('.')[0], + value: article.publish_at.toISOString().replace('T', ', ').split('.')[0], }), ]), m('div.input-group', [ @@ -287,7 +497,7 @@ const EditArticle = { this.data.staff.map((item) => { return m('option', { value: item.id, - selected: item.id === this.data.article.admin_id + selected: item.id === article.admin_id }, item.name) }) ), @@ -296,13 +506,13 @@ const EditArticle = { m('label', 'Make featured'), m('input', { type: 'checkbox', - checked: this.data.article.is_featured, + checked: article.is_featured, oninput: this.updateValue.bind(this, 'is_featured'), }), ]), ]), m('div', { - hidden: !this.data.article.name || !this.data.article.path + hidden: !article.name || !article.path }, [ m('input', { type: 'submit', @@ -311,7 +521,7 @@ const EditArticle = { showPublish ? m('button.submit', { onclick: () => { - this.data.article.publish_at = new Date().toISOString() + article.publish_at = new Date().toISOString() } }, 'Publish') : null, @@ -325,7 +535,7 @@ const EditArticle = { }), ]) : null, - this.data.article.id + article.id ? m('div.fileupload', [ 'Add file', m('input', { @@ -341,7 +551,7 @@ const EditArticle = { : m('div.error', { hidden: !this.error, onclick: () => { this.fetchArticle(vnode) }, - }, this.error),, + }, this.error),,*/ ] }, } diff --git a/nfp_moe/app/admin/site_editpage.js b/nfp_moe/app/admin/site_editpage.js index 56f567c..8ed307f 100644 --- a/nfp_moe/app/admin/site_editpage.js +++ b/nfp_moe/app/admin/site_editpage.js @@ -182,78 +182,84 @@ const AdminEditPage = { media: bannerImage, }, 'Click to upload banner image') : null, - m('div.inside.vertical', { - onsubmit: this.save.bind(this, vnode), - }, [ - m('div.page-goback', [ + m('div.inside.vertical', [ + m('div.actions', [ '« ', - m(m.route.Link, { href: '/admin/pages' }, 'Pages') + m(m.route.Link, { href: '/admin/pages' }, 'Pages'), + page + ? [ + m('div.filler'), + 'Actions:', + m(m.route.Link, { href: '/page/' + page.path }, 'View page'), + ] + : null, ]), m('h2.title', this.lastid === 'add' ? 'Create page' : 'Edit ' + (page && page.name || '(untitled)')), - page - ? m('div.actions', [ - m('span', 'Actions:'), - m(m.route.Link, { href: '/page/' + page.path }, 'View page'), - ]) - : null, - m('div.error', { - hidden: !this.error, - onclick: function() { vnode.state.error = '' }, - }, this.error), - this.loading - ? m('div.loading-spinner') - : [ - m(FileUpload, { - class: 'cover', - useimg: true, - onfile: this.mediaUploaded.bind(this, 'media'), - ondelete: this.mediaRemoved.bind(this, 'media'), - media: mediaImage, - }, 'Click to upload page image'), - m('form', [ - m('label', 'Parent'), - m('select', { - onchange: this.updateParent.bind(this), - }, this.pages.filter(item => !page || item.id !== page.id).map((item) => { - return m('option', { - value: item.id || 0, - selected: item.id === page.parent_id - }, item.name) - })), - m('div.input-row', [ - m('div', [ - m('label', 'Name'), - m('input', { - type: 'text', - value: page.name, - oninput: this.updateValue.bind(this, 'name'), - }), + m('div.container', [ + m('div.error', { + hidden: !this.error, + onclick: function() { vnode.state.error = '' }, + }, this.error), + this.loading + ? m('div.loading-spinner') + : null, + page + ? [ + m(FileUpload, { + class: 'cover', + useimg: true, + onfile: this.mediaUploaded.bind(this, 'media'), + ondelete: this.mediaRemoved.bind(this, 'media'), + media: mediaImage, + }, 'Click to upload page image'), + m('form', { + onsubmit: this.save.bind(this, vnode), + }, [ + m('label', 'Parent'), + m('select', { + onchange: this.updateParent.bind(this), + }, this.pages.filter(item => !page || item.id !== page.id).map((item) => { + return m('option', { + value: item.id || 0, + selected: item.id === page.parent_id + }, item.name) + })), + m('div.input-row', [ + m('div', [ + m('label', 'Name'), + m('input', { + type: 'text', + value: page.name, + oninput: this.updateValue.bind(this, 'name'), + }), + ]), + m('div', [ + m('label', 'Path'), + m('input', { + type: 'text', + value: page.path, + oninput: this.updateValue.bind(this, 'path'), + }), + ]), ]), - m('div', [ - m('label', 'Path'), - m('input', { - type: 'text', - value: page.path, - oninput: this.updateValue.bind(this, 'path'), - }), - ]), - ]), - m('label', 'Description'), - m('div.content', - m(Editor, { - oncreate: (subnode) => { - this.editor = subnode.state.editor - }, - contentdata: page.content, - }) - ), - m('input', { - hidden: !page.name || !page.path, - type: 'submit', - value: 'Save', - }), - ]) - ], + m('label', 'Description'), + m('div.content', + m(Editor, { + oncreate: (subnode) => { + this.editor = subnode.state.editor + }, + contentdata: page.content, + }) + ), + m('input', { + hidden: !page.name || !page.path, + type: 'submit', + value: 'Save', + }), + ]) + ] + : null, + ]), ]), ]), diff --git a/nfp_moe/app/admin/site_pages.js b/nfp_moe/app/admin/site_pages.js index 17eea78..d243c8e 100644 --- a/nfp_moe/app/admin/site_pages.js +++ b/nfp_moe/app/admin/site_pages.js @@ -6,7 +6,6 @@ const AdminPages = { oninit: function(vnode) { this.error = '' this.pages = [] - this.removePage = null document.title = 'Pages - Admin NFP Moe' this.fetchPages(vnode) @@ -32,24 +31,24 @@ const AdminPages = { }, confirmRemovePage: function(vnode, page) { - let removingPage = this.removePage - this.removePage = null this.loading = true m.redraw() return api.sendRequest({ method: 'DELETE', - url: '/api/auth/pages/' + removingPage.id, + url: '/api/auth/pages/' + page.id, }) - .then(() => PageTree.refreshTree()) .then( - () => this.fetchPages(vnode), - (err) => { - this.error = err.message - this.loading = false - m.redraw() - } + () => Promise.all([ + PageTree.refreshTree(), + this.fetchPages(), + ]), + (err) => { this.error = err.message } ) + .then(() => { + this.loading = false + m.redraw() + }) }, askConfirmRemovePage: function(vnode, page) { @@ -61,7 +60,7 @@ const AdminPages = { 'Don\'t remove', '', page, - this.confirmRemovePage.bind(this)) + this.confirmRemovePage.bind(this, vnode)) }, drawPage: function(vnode, page) { @@ -82,34 +81,32 @@ const AdminPages = { return [ m('div.admin', [ m('div.inside.vertical', [ - m('div.spacer'), - m('h2.title', 'All pages'), m('div.actions', [ + m('div.filler'), m('span', 'Actions:'), m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'), ]), - m('div.error', { - hidden: !this.error, - onclick: function() { vnode.state.error = '' }, - }, this.error), - this.loading - ? m('div.loading-spinner') - : m('table', [ - m('thead', - m('tr', [ - m('th', 'Title'), - m('th', 'Path'), - m('th.right', 'Updated'), - m('th.right', 'Actions'), - ]) - ), - m('tbody', this.pages.map(AdminPages.drawPage.bind(this, vnode))), - ], - ), - /*m(Pages, { - base: '/admin/articles', - links: this.links, - }),*/ + m('h2.title', 'All pages'), + m('div.container', [ + m('div.error', { + hidden: !this.error, + onclick: function() { vnode.state.error = '' }, + }, this.error), + this.loading + ? m('div.loading-spinner') + : m('table', [ + m('thead', + m('tr', [ + m('th', 'Title'), + m('th', 'Path'), + m('th.right', 'Updated'), + m('th.right', 'Actions'), + ]) + ), + m('tbody', this.pages.map(AdminPages.drawPage.bind(this, vnode))), + ], + ), + ]), ]), ]), ] diff --git a/nfp_moe/app/fileinfo.js b/nfp_moe/app/fileinfo.js index bac2d82..a5cca90 100644 --- a/nfp_moe/app/fileinfo.js +++ b/nfp_moe/app/fileinfo.js @@ -85,6 +85,7 @@ const Fileinfo = { && vnode.attrs.file.meta.torrent.files.length > 4 ? m('div.trimmed', '...' + vnode.attrs.file.meta.torrent.files.length + ' files...') : null, + vnode.children, ]) }, } diff --git a/nfp_moe/app/header.js b/nfp_moe/app/header.js index fbe7865..31692a2 100644 --- a/nfp_moe/app/header.js +++ b/nfp_moe/app/header.js @@ -73,7 +73,7 @@ const Menu = { 'Welcome ' + Authentication.currentUser.name + '. ', m('button', { onclick: this.logOut }, '(Log out)'), ]), - m('div.actions', [ + m('div', [ 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'), diff --git a/nfp_moe/app/site_article.js b/nfp_moe/app/site_article.js index 5858fc6..7b3d68a 100644 --- a/nfp_moe/app/site_article.js +++ b/nfp_moe/app/site_article.js @@ -90,12 +90,21 @@ const SiteArticle = { : null, (article ? m('.inside.vertical', [ - m('div.page-goback', ['« ', m(m.route.Link, { - href: article.page_path - ? '/page/' + article.page_path - : '/' - }, article.page_name || 'Home')] - ), + m('div.actions', [ + '« ', + m(m.route.Link, { + href: article.page_path + ? '/page/' + article.page_path + : '/' + }, article.page_name || 'Home'), + Authentication.currentUser + ? [ + m('div.filler'), + 'Actions:', + m(m.route.Link, { href: '/admin/articles/' + article.id }, 'Edit article'), + ] + : null, + ]), article ? m(Article, { full: true, files: this.data.files, article: article }) : null, window.LoadComments ? m('div#hyvor-talk-view', { oncreate: function() { diff --git a/nfp_moe/app/site_page.js b/nfp_moe/app/site_page.js index f2e0d50..07fe4f4 100644 --- a/nfp_moe/app/site_page.js +++ b/nfp_moe/app/site_page.js @@ -145,7 +145,7 @@ const SitePage = { : null), (page ? m('.inside.vertical', [ - m('div.page-goback', [ + m('div.actions', [ '« ', m(m.route.Link, { href: page.parent_path diff --git a/nfp_moe/public/assets/admin.css b/nfp_moe/public/assets/admin.css index 6e90297..d465dea 100644 --- a/nfp_moe/public/assets/admin.css +++ b/nfp_moe/public/assets/admin.css @@ -3,6 +3,7 @@ */ :root { --admin-bg: hsl(213.9, 100%, 95%); + --admin-bg-highlight: hsl(213.9, 100%, 85%); --admin-color: #000; --admin-table-border: #01579b; --admin-table-header-bg: #3D77C7; @@ -18,27 +19,27 @@ .admin { background: var(--admin-bg); color: var(--admin-color); - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - min-height: calc(100vh - 200px); -} - -.admin .inside { - padding: 0 1rem 1rem; -} - -.admin .spacer { - height: 40px; + min-height: calc(100vh - 390px); } .admin .loading-spinner { - position: relative; - left: unset; - top: unset; - min-height: 300px; - height: calc(100vh - 300px); + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: #0002 +} + +.admin .container .actions { + margin-left: -2rem; +} + +.admin .container .actions button, +.admin .container .actions input { + margin-left: 1rem; + font-weight: normal; + min-width: 150px; } .admin table { @@ -47,6 +48,14 @@ border-spacing: 0; font-size: 0.75em; margin-bottom: 1rem; + width: 100%; +} + +input[type=checkbox] { + display: block; + height: 20px; + margin: 0.5rem 0; + width: 20px; } .admin table thead th, @@ -65,6 +74,10 @@ color: var(--alt-color); } +.admin table tr:hover td { + background: var(--admin-bg-highlight); +} + .admin table button { color: var(--link); background: transparent; @@ -78,17 +91,6 @@ text-align: right; } -.admin .actions { - margin: 0.5rem 0; - display: flex; - justify-content: flex-end; - font-size: 0.875rem; -} - -.admin .actions a { - margin-left: 0.5rem; -} - .admin form { margin: 1rem 0 0; } @@ -108,12 +110,55 @@ flex: 2 1 50px; } +.admin .input-row > .slim { + flex: 0 0 auto; +} + +.admin .error { + position: fixed; + top: 0; + left: 50%; + margin-left: -30%; + width: 60%; + z-index: 10; +} + +/* ************** fileinfo ************** */ + +.admin fileinfo:hover { + background: var(--admin-bg-highlight); +} + +.admin fileinfo .remove { + position: absolute; + right: 0; + top: 0; + height: 100%; + background: linear-gradient(to right, transparent, var(--admin-bg) 2rem); + padding-left: 3rem; + display: flex; + align-items: center; + justify-content: center; +} + +.admin fileinfo:hover .remove { + background: linear-gradient(to right, transparent, var(--admin-bg-highlight) 2rem); +} + +.admin fileinfo .remove button { + margin: 0; +} + /* ************** fileupload ************** */ fileupload { + width: 100%; +} + +fileupload, +.fileupload { position: relative; display: block; - width: 100%; } fileupload.banner { @@ -151,7 +196,8 @@ fileupload .text { color: var(--seperator); } -fileupload input { +fileupload input, +.fileupload input { position: absolute; top: 0; left: 0; @@ -189,9 +235,6 @@ dialogue { color: var(--color); } -dialogue h2 { -} - dialogue p { padding: 1rem; } @@ -233,6 +276,8 @@ dialogue button.cancel { ===================== 3rd party ===================== */ +/* ************** Editor ************** */ + .ce-block__content, .ce-toolbar__content { max-width:calc(100% - 120px) !important; } .cdx-block { max-width: 100% !important; } @@ -241,6 +286,7 @@ dialogue button.cancel { border: 1px solid var(--color); background: var(--bg); color: var(--color); + padding-top: 0.5rem; } .codex-editor:hover, @@ -248,3 +294,102 @@ dialogue button.cancel { border-color: var(--link); } +/* ************** dte ************** */ + +.date-selector-wrapper { + width: 200px; + padding: 3px; + background-color: #fff; + box-shadow: 1px 1px 10px 1px #5c5c5c; + position: absolute; + font-size: 12px; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + z-index: 10; + /* user-select: none; */ +} +.cal-header, .cal-row { + display: flex; + width: 100%; + height: 30px; + line-height: 30px; + text-align: center; +} +.cal-cell, .cal-nav { + cursor: pointer; +} +.cal-day-names { + height: 25px; + line-height: 25px; +} +.cal-day-names .cal-cell { + cursor: default; + font-weight: bold; +} +.cal-cell-prev, .cal-cell-next { + color: #777; +} +.cal-months .cal-row, .cal-years .cal-row { + height: 60px; + line-height: 60px; +} +.cal-nav-prev, .cal-nav-next { + flex: 0.15; +} +.cal-nav-current { + flex: 0.75; + font-weight: bold; +} +.cal-months .cal-cell, .cal-years .cal-cell { + flex: 0.25; +} +.cal-days .cal-cell { + flex: 0.143; +} +.cal-value { + color: #fff; + background-color: #286090; +} +.cal-cell:hover, .cal-nav:hover { + background-color: #eee; +} +.cal-value:hover { + background-color: #204d74; +} + +/* time footer */ +.cal-time { + display: flex; + justify-content: flex-start; + height: 27px; + line-height: 27px; +} +.cal-time-label, .cal-time-value { + flex: 0.12; + text-align: center; +} +.cal-time-slider { + flex: 0.77; + background-image: linear-gradient(to right, #d1d8dd, #d1d8dd); + background-repeat: no-repeat; + background-size: 100% 1px; + background-position: left 50%; + height: 100%; +} +.cal-time-slider input { + width: 100%; + -webkit-appearance: none; + background: 0 0; + cursor: pointer; + height: 100%; + outline: 0; + user-select: auto; +} + +.ce-block__content, +.ce-toolbar__content { max-width:calc(100% - 120px) !important; } +.cdx-block { max-width: 100% !important; } + diff --git a/nfp_moe/public/assets/app.css b/nfp_moe/public/assets/app.css index 89bd92f..db6dba9 100644 --- a/nfp_moe/public/assets/app.css +++ b/nfp_moe/public/assets/app.css @@ -82,6 +82,7 @@ a { img { max-width: 100%; + margin: 0 auto; display: block; } @@ -130,6 +131,7 @@ select { color: var(--color); border-radius: 0; padding: 0.25rem; + line-height: 1rem; } label { @@ -272,9 +274,13 @@ header aside { padding: 0.5rem 0.5rem; } -header aside .actions a { - margin-left: 1rem; - display: inline-block; +header aside a, +header aside button { + margin-left: 0.5rem; +} + +header aside p button { + margin-left: 0; } .avifsupport header .logo { @@ -335,12 +341,12 @@ main { text-shadow: 0 0 .3em #000; } -.page-goback { +.actions { padding: 0.5rem 1rem; display: flex; } -.page-goback a { +.actions a { margin-left: 0.375rem; } @@ -520,6 +526,7 @@ fileinfo { line-height: 1rem; font-size: 0.75rem; display: block; + position: relative; } fileinfo.slim { diff --git a/nfp_moe/public/assets/img/correct_asuna_frontpage.xcf b/nfp_moe/public/assets/img/correct_asuna_frontpage.xcf new file mode 100644 index 0000000..3a3d483 Binary files /dev/null and b/nfp_moe/public/assets/img/correct_asuna_frontpage.xcf differ diff --git a/nfp_moe/public/assets/img/correct_asuna_frontpage_night.xcf b/nfp_moe/public/assets/img/correct_asuna_frontpage_night.xcf new file mode 100644 index 0000000..5b6b4b5 Binary files /dev/null and b/nfp_moe/public/assets/img/correct_asuna_frontpage_night.xcf differ diff --git a/nfp_moe/public/assets/img/correct_footer.xcf b/nfp_moe/public/assets/img/correct_footer.xcf new file mode 100644 index 0000000..643ad27 Binary files /dev/null and b/nfp_moe/public/assets/img/correct_footer.xcf differ diff --git a/nfp_moe/public/assets/img/correct_logo.xcf b/nfp_moe/public/assets/img/correct_logo.xcf new file mode 100644 index 0000000..e23309a Binary files /dev/null and b/nfp_moe/public/assets/img/correct_logo.xcf differ