Backed up old npf_moe, removed all scss from nfp_moe.

master
Jonatan Nilsson 2022-08-02 12:34:52 +00:00
parent eb9ffde724
commit 296bf51c9a
98 changed files with 4732 additions and 85 deletions

View File

@ -1,4 +1,5 @@
import { parseFile } from '../file/util.mjs'
import { contentToBlocks, parseMediaAndBanner } from '../util.mjs'
export function parseArticles(articles) {
for (let i = 0; i < articles.length; i++) {
@ -23,36 +24,7 @@ export function parseArticle(article) {
if (!article) {
return null
}
if (article.content) {
if (article.content[0] === '{') {
try {
article.content = JSON.parse(article.content)
} catch (err) {
article.content = {
time: new Date().getTime(),
blocks: [
{id: '1', type: 'paragraph', data: { text: 'Error parsing article content: ' + err.message }},
],
version: '2.25.0'
}
}
} else {
article.content = {
time: new Date().getTime(),
blocks: [
{id: '1', type: 'htmlraw', data: { html: article.content }},
],
version: '2.25.0'
}
}
}
if (article.banner_path) {
article.banner_path = 'https://cdn.nfp.is' + article.banner_path
article.banner_alt_prefix = 'https://cdn.nfp.is' + article.banner_alt_prefix
}
if (article.media_path) {
article.media_path = 'https://cdn.nfp.is' + article.media_path
article.media_alt_prefix = 'https://cdn.nfp.is' + article.media_alt_prefix
}
article.content = contentToBlocks(article.content)
parseMediaAndBanner(article)
return article
}

View File

@ -1,4 +1,4 @@
import { parsePagesToTree } from './util.mjs'
import { parsePage, parsePagesToTree } from './util.mjs'
import { upload } from '../media/upload.mjs'
import { combineFilesWithArticles, parseArticle, parseArticles } from '../article/util.mjs'
import { mediaToDatabase } from '../media/util.mjs'
@ -43,7 +43,7 @@ export default class PageRoutes {
])
let out = {
page: res.results[0][0] || null,
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]),
@ -83,7 +83,7 @@ export default class PageRoutes {
let res = await ctx.db.safeCallProc('pages_auth_get_update_create', params)
let out = {
page: res.results[0][0] || {},
page: parsePage(res.results[0][0] || {}),
}
ctx.body = out

View File

@ -1,3 +1,5 @@
import { contentToBlocks, parseMediaAndBanner } from '../util.mjs'
export function parsePagesToTree(pages) {
let out = []
let children = []
@ -20,4 +22,13 @@ export function parsePagesToTree(pages) {
return {
tree: out
}
}
export function parsePage(page) {
if (!page) {
return null
}
page.content = contentToBlocks(page.content)
parseMediaAndBanner(page)
return page
}

View File

@ -19,3 +19,40 @@ export function decode(base64StringUrlSafe) {
}
return Buffer.from(base64String, 'base64')
}
export function parseMediaAndBanner(item) {
if (item.banner_path) {
item.banner_path = 'https://cdn.nfp.is' + item.banner_path
item.banner_alt_prefix = 'https://cdn.nfp.is' + item.banner_alt_prefix
}
if (item.media_path) {
item.media_path = 'https://cdn.nfp.is' + item.media_path
item.media_alt_prefix = 'https://cdn.nfp.is' + item.media_alt_prefix
}
}
export function contentToBlocks(content) {
if (!content) return content
if (content[0] === '{') {
try {
return JSON.parse(content)
} catch (err) {
return {
time: new Date().getTime(),
blocks: [
{id: '1', type: 'paragraph', data: { text: 'Error parsing content: ' + err.message }},
],
version: '2.25.0'
}
}
}
return {
time: new Date().getTime(),
blocks: [
{id: '1', type: 'htmlraw', data: { html: content }},
],
version: '2.25.0'
}
}

View File

@ -12,10 +12,6 @@ const Article = {
this.data = {
article: null,
files: [],
pictureFallback: null,
pictureJpeg: null,
pictureAvif: null,
pictureCover: null,
}
this.showcomments = false

View File

@ -70,6 +70,24 @@ const Page = {
return
}
if (this.data.page.media_alt_prefix) {
this.data.page.pictureFallback = this.data.page.media_alt_prefix + '_small.jpg'
this.data.page.pictureJpeg = this.data.page.media_alt_prefix + '_small.jpg' + ' 720w, '
+ this.data.page.media_alt_prefix + '_medium.jpg' + ' 1300w, '
+ this.data.page.media_alt_prefix + '_large.jpg 1920w'
this.data.page.pictureAvif = this.data.page.media_alt_prefix + '_small.avif' + ' 720w, '
+ this.data.page.media_alt_prefix + '_medium.avif' + ' 1300w, '
+ this.data.page.media_alt_prefix + '_large.avif 1920w'
this.data.page.pictureCover = '(max-width: 840px) calc(100vw - 82px), '
+ '758px'
} else {
this.data.page.pictureFallback = null
this.data.page.pictureJpeg = null
this.data.page.pictureAvif = null
this.data.page.pictureCover = null
}
if (this.lastpage !== 1) {
document.title = 'Page ' + this.lastpage + ' - ' + this.data.page.name + ' - NFP Moe'
} else {
@ -87,30 +105,8 @@ const Page = {
},
view: function(vnode) {
var deviceWidth = window.innerWidth
var pixelRatio = window.devicePixelRatio || 1
var bannerPath = ''
var imagePath = ''
if (this.data.page && this.data.page.banner) {
if (deviceWidth < 400 && pixelRatio <= 1) {
bannerPath = this.data.page.banner.small_url
} else if ((deviceWidth < 800 && pixelRatio <= 1)
|| (deviceWidth < 600 && pixelRatio > 1)) {
bannerPath = this.data.page.banner.medium_url
} else {
bannerPath = this.data.page.banner.large_url
}
}
if (this.data.page && this.data.page.media) {
if ((deviceWidth < 1000 && pixelRatio <= 1)
|| (deviceWidth < 800 && pixelRatio > 1)) {
imagePath = this.data.page.media.medium_url
} else {
imagePath = this.data.page.media.large_url
}
}
let page = this.data.page
let bannerPath = ''
return ([
this.loading
@ -130,36 +126,55 @@ const Page = {
? m('.div.page-banner', { style: { 'background-image': 'url("' + bannerPath + '")' } } )
: null,
m('div.goback', ['« ', m(m.route.Link, {
href: this.data.page.parent_path
? '/page/' + this.data.page.parent_path
href: page.parent_path
? '/page/' + page.parent_path
: '/'
}, this.data.page.parent_name || 'Home')]),
m('header', m('h1', this.data.page.name)),
}, page.parent_name || 'Home')]),
m('header', m('h1', page.name)),
m('.container', {
class: this.children.length ? 'multi' : '',
}, [
this.children.length
? m('aside.sidebar', [
m('h4', 'View ' + this.data.page.name + ':'),
m('h4', 'View ' + page.name + ':'),
this.children.map(function(page) {
return m(m.route.Link, { href: '/page/' + page.path }, page.name)
}),
])
: null,
this.data.page.content
page.content
? m('.fr-view', [
imagePath
? m('a', { href: this.data.page.media.link}, m('img.page-cover', { src: imagePath, alt: 'Cover image for ' + this.data.page.name } ))
: null,
m.trust(this.data.page.content),
this.data.articles.length && this.data.page.content
page.pictureFallback
? m('a.cover', {
rel: 'noopener',
href: page.media_path,
}, [
m('picture', [
m('source', {
srcset: page.pictureAvif,
sizes: page.pictureCover,
type: 'image/avif',
}),
m('img', {
srcset: page.pictureJpeg,
sizes: page.pictureCover,
alt: 'Image for news item ' + page.name,
src: page.pictureFallback,
}),
]),
])
: null,
page.content.blocks.map(block => {
return m(EditorBlock, { block: block })
}),
this.data.articles.length && page.content
? m('aside.news', [
m('h4', 'Latest posts under ' + this.data.page.name + ':'),
m('h4', 'Latest posts under ' + page.name + ':'),
this.data.articles.map(function(article) {
return m(Newsentry, { article: article })
}),
m(Pages, {
base: '/page/' + this.data.page.path,
base: '/page/' + page.path,
total: this.data.total_articles,
page: this.currentPage,
}),
@ -167,26 +182,46 @@ const Page = {
: null,
])
: this.data.articles.length
? m('aside.news.single', [
imagePath ? m('a', { href: this.data.page.media.link}, m('img.page-cover', { src: imagePath, alt: 'Cover image for ' + this.data.page.name } )) : null,
m('h4', 'Latest posts under ' + this.data.page.name + ':'),
? m('aside.news.single',
[
page.pictureFallback
? m('a', {
rel: 'noopener',
href: page.media_path,
}, [
m('picture.page-cover', [
m('source', {
srcset: page.pictureAvif,
sizes: page.pictureCover,
type: 'image/avif',
}),
m('img', {
srcset: page.pictureJpeg,
sizes: page.pictureCover,
alt: 'Cover image for ' + page.name,
src: page.pictureFallback,
}),
]),
])
: null,
m('h4', 'Latest posts under ' + page.name + ':'),
this.data.articles.map(function(article) {
return m(Newsentry, { article: article })
}),
m(Pages, {
base: '/page/' + this.data.page.path,
base: '/page/' + page.path,
total: this.data.total_articles,
page: this.currentPage,
}),
])
: this.data.page.media
? m('img.page-cover.single', { src: this.data.page.media.medium_url, alt: 'Cover image for ' + this.data.page.name } )
: page.media
? m('img.page-cover.single', { src: page.media.medium_url, alt: 'Cover image for ' + page.name } )
: null,
]),
Authentication.currentUser
? m('div.admin-actions', [
m('span', 'Admin controls:'),
m(m.route.Link, { href: '/admin/pages/' + this.data.page.path }, 'Edit page'),
m(m.route.Link, { href: '/admin/pages/' + page.path }, 'Edit page'),
])
: null,
])

View File

@ -9,8 +9,8 @@
"scripts": {
"start": "node --experimental-modules index.mjs",
"test": "echo \"Error: no test specified\" && exit 1",
"build:prod": "sass -s compressed app/app.scss public/assets/app.css && sass -s compressed app/admin.scss public/assets/admin.css && asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js",
"build": "sass app/app.scss public/assets/app.css && sass app/admin.scss public/assets/admin.css && asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js",
"build:prod": "asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js",
"build": "asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js",
"dev:build": "npm-watch build",
"dev:server": "node dev.mjs | bunyan",
"dev": "npm-watch dev:server",
@ -23,7 +23,8 @@
"dev:server": {
"patterns": [
"api/*",
"base/*"
"base/*",
"../base/*"
],
"extensions": "js,mjs",
"quiet": true,

1
nfp_moe_old/.npmrc Normal file
View File

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

View File

@ -0,0 +1,18 @@
import config from '../base/config.mjs'
import Parent from '../base/server.mjs'
import ServeHandler from '../base/serve.mjs'
import PageRoutes from '../base/page/routes.mjs'
export default class Server extends Parent {
addCustomRoutes() {
let page = this.getRouteInstance(PageRoutes)
let localUtil = new this.core.sc.Util(import.meta.url)
this.routes.push(new ServeHandler({
pageRoutes: page,
root: localUtil.getPathFromRoot('../public'),
version: this.core.app.running,
frontend: config.get('frontend:url'),
}))
}
}

View File

@ -0,0 +1,162 @@
const Article = require('../api/article')
const pagination = require('../api/pagination')
const Dialogue = require('../widgets/dialogue')
const Pages = require('../widgets/pages')
const common = require('../api/common')
const AdminArticles = {
oninit: function(vnode) {
this.error = ''
this.loading = false
this.showLoading = null
this.data = {
articles: [],
total_articles: 0,
}
this.removeArticle = null
this.currentPage = Number(m.route.param('page')) || 1
this.fetchArticles(vnode)
},
onbeforeupdate: function(vnode) {
this.currentPage = Number(m.route.param('page')) || 1
if (this.currentPage !== this.lastpage) {
this.fetchArticles(vnode)
}
},
fetchArticles: function(vnode) {
this.error = ''
this.lastpage = this.currentPage
document.title = 'Articles Page ' + this.lastpage + ' - Admin NFP Moe'
if (this.showLoading) {
clearTimeout(this.showLoading)
}
if (this.data.articles.length) {
this.showLoading = setTimeout(() => {
this.showLoading = null
this.loading = true
m.redraw()
}, 150)
} else {
this.loading = true
}
return common.sendRequest({
method: 'GET',
url: '/api/auth/articles?page=' + (this.lastpage || 1),
})
.then((result) => {
this.data = result
this.data.articles.forEach((article) => {
article.hidden = new Date() < new Date(article.publish_at)
article.page_path = article.page_path ? '/page/' + article.page_path : '/'
article.page_name = article.page_name || 'Frontpage'
})
}, (err) => {
this.error = err.message
})
.then(() => {
clearTimeout(this.showLoading)
this.showLoading = null
this.loading = false
m.redraw()
})
},
confirmRemoveArticle: function(vnode) {
let removingArticle = this.removeArticle
this.removeArticle = null
this.loading = true
m.redraw()
return common.sendRequest({
method: 'DELETE',
url: '/api/auth/articles/' + removingArticle.id,
})
.then(
() => this.fetchArticles(vnode),
(err) => {
this.error = err.message
this.loading = false
m.redraw()
}
)
},
drawArticle: function(vnode, article) {
return [
m('tr', {
class: article.hidden
? rowhidden
: article.is_featured
? 'rowfeatured'
: ''
}, [
m('td', m(m.route.Link, { href: '/admin/articles/' + article.id }, article.name)),
m('td', m(m.route.Link, { href: article.page_path }, article.page_name)),
m('td', m(m.route.Link, { href: '/article/' + article.path }, '/article/' + article.path)),
m('td.right', article.publish_at.replace('T', ' ').split('.')[0]),
m('td.right', article.admin_name),
m('td.right', m('button', { onclick: function() { vnode.state.removeArticle = article } }, 'Remove')),
]),
]
},
view: function(vnode) {
return [
m('div.admin-wrapper', [
m('div.admin-actions', [
m('span', 'Actions:'),
m(m.route.Link, { href: '/admin/articles/add' }, 'Create new article'),
]),
m('article.editarticle', [
m('header', m('h1', 'All articles')),
m('div.error', {
hidden: !this.error,
onclick: function() { vnode.state.error = '' },
}, this.error),
this.loading
? m('div.loading-spinner.full')
: m('table', [
m('thead',
m('tr', [
m('th', 'Title'),
m('th', 'Page'),
m('th', 'Path'),
m('th.right', 'Publish'),
m('th.right', 'By'),
m('th.right', 'Actions'),
])
),
m('tbody', this.data.articles.map((article) => this.drawArticle(vnode, article))),
],
),
/*m(Pages, {
base: '/admin/articles',
links: this.links,
}),*/
]),
]),
m(Dialogue, {
hidden: vnode.state.removeArticle === null,
title: 'Delete ' + (vnode.state.removeArticle ? vnode.state.removeArticle.name : ''),
message: 'Are you sure you want to remove "' + (vnode.state.removeArticle ? vnode.state.removeArticle.name : '') + '" (' + (vnode.state.removeArticle ? vnode.state.removeArticle.path : '') + ')',
yes: 'Remove',
yesclass: 'alert',
no: 'Cancel',
noclass: 'cancel',
onyes: this.confirmRemoveArticle.bind(this, vnode),
onno: function() { vnode.state.removeArticle = null },
}),
]
},
}
module.exports = AdminArticles

View File

@ -0,0 +1,950 @@
(function () {
"use strict";
var BODYTYPES = ["DAYS", "MONTHS", "YEARS"];
/** @typedef {Object.<string, Function[]>} Handlers */
/** @typedef {function(String, Function): null} AddHandler */
/** @typedef {("DAYS"|"MONTHS"|"YEARS")} BodyType */
/** @typedef {string|number} StringNum */
/** @typedef {Object.<string, StringNum>} StringNumObj */
/**
* The local state
* @typedef {Object} InstanceState
* @property {Date} value
* @property {Number} year
* @property {Number} month
* @property {Number} day
* @property {Number} time
* @property {Number} hours
* @property {Number} minutes
* @property {Number} seconds
* @property {BodyType} bodyType
* @property {Boolean} visible
* @property {Number} cancelBlur
*/
/**
* @typedef {Object} Config
* @property {String} dateFormat
* @property {String} timeFormat
* @property {Boolean} showDate
* @property {Boolean} showTime
* @property {Boolean} showSeconds
* @property {Number} paddingX
* @property {Number} paddingY
* @property {BodyType} defaultView
* @property {"TOP"|"BOTTOM"} direction
* @property {Array} months
* @property {Array} monthsShort
* @property {Array} weekdaysShort
* @property {Array} timeDescr
*/
/**
* @class
* @param {HTMLElement} elem
* @param {Config} config
*/
function DTS(elem, config) {
var config = config || {};
/** @type {Config} */
var defaultConfig = {
defaultView: BODYTYPES[0],
dateFormat: "yyyy-mm-dd",
timeFormat: "HH:MM:SS",
showDate: true,
showTime: false,
showSeconds: true,
paddingX: 5,
paddingY: 5,
direction: 'TOP',
months: [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
],
monthsShort: [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
],
weekdaysShort: [
"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"
],
timeDescr: [
"HH:", "MM:", "SS:"
]
}
if (!elem) {
throw TypeError("input element or selector required for contructor");
}
if (Object.getPrototypeOf(elem) === String.prototype) {
var _elem = document.querySelectorAll(elem);
if (!_elem[0]){
throw Error('"' + elem + '" not found.');
}
elem = _elem[0];
}
this.config = setDefaults(config, defaultConfig);
this.dateFormat = this.config.dateFormat;
this.timeFormat = this.config.timeFormat;
this.dateFormatRegEx = new RegExp("yyyy|yy|mm|dd", "gi");
this.timeFormatRegEx = new RegExp("hh|mm|ss|a", "gi");
this.inputElem = elem;
this.dtbox = null;
this.setup();
}
DTS.prototype.setup = function () {
var handler = this.inputElemHandler.bind(this);
this.inputElem.addEventListener("focus", handler, false)
this.inputElem.addEventListener("blur", handler, false);
}
DTS.prototype.inputElemHandler = function (e) {
if (e.type == "focus") {
if (!this.dtbox) {
this.dtbox = new DTBox(e.target, this);
}
this.dtbox.visible = true;
} else if (e.type == "blur" && this.dtbox && this.dtbox.visible) {
var self = this;
setTimeout(function () {
if (self.dtbox.cancelBlur > 0) {
self.dtbox.cancelBlur -= 1;
} else {
self.dtbox.visible = false;
self.inputElem.blur();
}
}, 100);
}
}
/**
* @class
* @param {HTMLElement} elem
* @param {DTS} settings
*/
function DTBox(elem, settings) {
/** @type {DTBox} */
var self = this;
/** @type {Handlers} */
var handlers = {};
/** @type {InstanceState} */
var localState = {};
/**
* @param {String} key
* @param {*} default_val
*/
function getterSetter(key, default_val) {
return {
get: function () {
var val = localState[key];
return val === undefined ? default_val : val;
},
set: function (val) {
var prevState = self.state;
var _handlers = handlers[key] || [];
localState[key] = val;
for (var i = 0; i < _handlers.length; i++) {
_handlers[i].bind(self)(localState, prevState);
}
},
};
};
/** @type {AddHandler} */
function addHandler(key, handlerFn) {
if (!key || !handlerFn) {
return false;
}
if (!handlers[key]) {
handlers[key] = [];
}
handlers[key].push(handlerFn);
}
Object.defineProperties(this, {
visible: getterSetter("visible", false),
bodyType: getterSetter("bodyType", settings.config.defaultView),
value: getterSetter("value"),
year: getterSetter("year", 0),
month: getterSetter("month", 0),
day: getterSetter("day", 0),
hours: getterSetter("hours", 0),
minutes: getterSetter("minutes", 0),
seconds: getterSetter("seconds", 0),
cancelBlur: getterSetter("cancelBlur", 0),
addHandler: {value: addHandler},
month_long: {
get: function () {
return self.settings.config.months[self.month];
},
},
month_short: {
get: function () {
return self.settings.config.monthsShort[self.month]
},
},
state: {
get: function () {
return Object.assign({}, localState);
},
},
time: {
get: function() {
var hours = self.hours * 60 * 60 * 1000;
var minutes = self.minutes * 60 * 1000;
var seconds = self.seconds * 1000;
return hours + minutes + seconds;
}
},
});
this.el = {};
this.settings = settings;
this.elem = elem;
this.setup();
}
DTBox.prototype.setup = function () {
Object.defineProperties(this.el, {
wrapper: { value: null, configurable: true },
header: { value: null, configurable: true },
body: { value: null, configurable: true },
footer: { value: null, configurable: true }
});
this.setupWrapper();
if (this.settings.config.showDate) {
this.setupHeader();
this.setupBody();
}
if (this.settings.config.showTime) {
this.setupFooter();
}
var self = this;
this.addHandler("visible", function (state, prevState) {
if (state.visible && !prevState.visible){
document.body.appendChild(this.el.wrapper);
var parts = self.elem.value.split(/\s*,\s*/);
var startDate = undefined;
var startTime = 0;
if (self.settings.config.showDate) {
startDate = parseDate(parts[0], self.settings);
}
if (self.settings.config.showTime) {
startTime = parseTime(parts[parts.length-1], self.settings);
startTime = startTime || 0;
}
if (!(startDate && startDate.getTime())) {
startDate = new Date();
startDate = new Date(
startDate.getFullYear(),
startDate.getMonth(),
startDate.getDate()
);
}
var value = new Date(startDate.getTime() + startTime);
self.value = value;
self.year = value.getFullYear();
self.month = value.getMonth();
self.day = value.getDate();
self.hours = value.getHours();
self.minutes = value.getMinutes();
self.seconds = value.getSeconds();
if (self.settings.config.showDate) {
self.setHeaderContent();
self.setBodyContent();
}
if (self.settings.config.showTime) {
self.setFooterContent();
}
} else if (!state.visible && prevState.visible) {
document.body.removeChild(this.el.wrapper);
}
});
}
DTBox.prototype.setupWrapper = function () {
if (!this.el.wrapper) {
var el = document.createElement("div");
el.classList.add("date-selector-wrapper");
Object.defineProperty(this.el, "wrapper", { value: el });
}
var self = this;
var htmlRoot = document.getElementsByTagName('html')[0];
function setPosition(e){
var minTopSpace = 300;
var box = getOffset(self.elem);
var config = self.settings.config;
var paddingY = config.paddingY || 5;
var paddingX = config.paddingX || 5;
var top = box.top + self.elem.offsetHeight + paddingY;
var left = box.left + paddingX;
var bottom = htmlRoot.clientHeight - box.top + paddingY;
self.el.wrapper.style.left = `${left}px`;
if (box.top > minTopSpace && config.direction != 'BOTTOM') {
self.el.wrapper.style.bottom = `${bottom}px`;
self.el.wrapper.style.top = '';
} else {
self.el.wrapper.style.top = `${top}px`;
self.el.wrapper.style.bottom = '';
}
}
function handler(e) {
self.cancelBlur += 1;
setTimeout(function(){
self.elem.focus();
}, 50);
}
setPosition();
this.setPosition = setPosition;
this.el.wrapper.addEventListener("mousedown", handler, false);
this.el.wrapper.addEventListener("touchstart", handler, false);
window.addEventListener('resize', this.setPosition);
}
DTBox.prototype.setupHeader = function () {
if (!this.el.header) {
var row = document.createElement("div");
var classes = ["cal-nav-prev", "cal-nav-current", "cal-nav-next"];
row.classList.add("cal-header");
for (var i = 0; i < 3; i++) {
var cell = document.createElement("div");
cell.classList.add("cal-nav", classes[i]);
cell.onclick = this.onHeaderChange.bind(this);
row.appendChild(cell);
}
row.children[0].innerHTML = "&lt;";
row.children[2].innerHTML = "&gt;";
Object.defineProperty(this.el, "header", { value: row });
tryAppendChild(row, this.el.wrapper);
}
this.setHeaderContent();
}
DTBox.prototype.setHeaderContent = function () {
var content = this.year;
if ("DAYS" == this.bodyType) {
content = this.month_long + " " + content;
} else if ("YEARS" == this.bodyType) {
var start = this.year + 10 - (this.year % 10);
content = start - 10 + "-" + (start - 1);
}
this.el.header.children[1].innerText = content;
}
DTBox.prototype.setupBody = function () {
if (!this.el.body) {
var el = document.createElement("div");
el.classList.add("cal-body");
Object.defineProperty(this.el, "body", { value: el });
tryAppendChild(el, this.el.wrapper);
}
var toAppend = null;
function makeGrid(rows, cols, className, firstRowClass, clickHandler) {
var grid = document.createElement("div");
grid.classList.add(className);
for (var i = 1; i < rows + 1; i++) {
var row = document.createElement("div");
row.classList.add("cal-row", "cal-row-" + i);
if (i == 1 && firstRowClass) {
row.classList.add(firstRowClass);
}
for (var j = 1; j < cols + 1; j++) {
var col = document.createElement("div");
col.classList.add("cal-cell", "cal-col-" + j);
col.onclick = clickHandler;
row.appendChild(col);
}
grid.appendChild(row);
}
return grid;
}
if ("DAYS" == this.bodyType) {
toAppend = this.el.body.calDays;
if (!toAppend) {
toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this));
for (var i = 0; i < 7; i++) {
var cell = toAppend.children[0].children[i];
cell.innerText = this.settings.config.weekdaysShort[i];
cell.onclick = null;
}
this.el.body.calDays = toAppend;
}
} else if ("MONTHS" == this.bodyType) {
toAppend = this.el.body.calMonths;
if (!toAppend) {
toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this));
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 4; j++) {
var monthShort = this.settings.config.monthsShort[4 * i + j];
toAppend.children[i].children[j].innerText = monthShort;
}
}
this.el.body.calMonths = toAppend;
}
} else if ("YEARS" == this.bodyType) {
toAppend = this.el.body.calYears;
if (!toAppend) {
toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this));
this.el.body.calYears = toAppend;
}
}
empty(this.el.body);
tryAppendChild(toAppend, this.el.body);
this.setBodyContent();
}
DTBox.prototype.setBodyContent = function () {
var grid = this.el.body.children[0];
var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"];
if ("DAYS" == this.bodyType) {
var oneDayMilliSecs = 24 * 60 * 60 * 1000;
var start = new Date(this.year, this.month, 1);
var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay());
grid.children[6].style.display = "";
for (var i = 1; i < 7; i++) {
for (var j = 0; j < 7; j++) {
var cell = grid.children[i].children[j];
var month = adjusted.getMonth();
var date = adjusted.getDate();
cell.innerText = date;
cell.classList.remove(classes[0], classes[1], classes[2]);
if (month != this.month) {
if (i == 6 && j == 0) {
grid.children[6].style.display = "none";
break;
}
cell.classList.add(month < this.month ? classes[0] : classes[1]);
} else if (isEqualDate(adjusted, this.value)){
cell.classList.add(classes[2]);
}
adjusted = new Date(adjusted.getTime() + oneDayMilliSecs);
}
}
} else if ("YEARS" == this.bodyType) {
var year = this.year - (this.year % 10) - 1;
for (i = 0; i < 3; i++) {
for (j = 0; j < 4; j++) {
grid.children[i].children[j].innerText = year;
year += 1;
}
}
grid.children[0].children[0].classList.add(classes[0]);
grid.children[2].children[3].classList.add(classes[1]);
}
}
/** @param {Event} e */
DTBox.prototype.onTimeChange = function(e) {
e.stopPropagation();
if (e.type == 'mousedown') {
this.cancelBlur += 1;
return;
}
if (e.type == 'mouseup') {
var self = this;
setTimeout(function(){
self.elem.focus();
}, 50);
return;
}
var el = e.target;
this[el.name] = parseInt(el.value) || 0;
this.setupFooter();
this.setInputValue();
}
DTBox.prototype.setupFooter = function() {
if (!this.el.footer) {
var footer = document.createElement("div");
var handler = this.onTimeChange.bind(this);
var self = this;
function makeRow(label, name, range, changeHandler) {
var row = document.createElement("div");
row.classList.add('cal-time');
var labelCol = row.appendChild(document.createElement("div"));
labelCol.classList.add('cal-time-label');
labelCol.innerText = label;
var valueCol = row.appendChild(document.createElement("div"));
valueCol.classList.add('cal-time-value');
valueCol.innerText = '00';
var inputCol = row.appendChild(document.createElement("div"));
var slider = inputCol.appendChild(document.createElement("input"));
Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'});
Object.defineProperty(footer, name, {value: slider});
inputCol.classList.add('cal-time-slider');
slider.onchange = changeHandler;
slider.oninput = changeHandler;
slider.onmousedown = changeHandler;
slider.onmouseup = changeHandler;
self[name] = self[name] || parseInt(slider.value) || 0;
footer.appendChild(row)
}
makeRow(this.settings.config.timeDescr[0], 'hours', 23, handler);
makeRow(this.settings.config.timeDescr[1], 'minutes', 59, handler);
if (this.settings.config.showSeconds) {
makeRow(this.settings.config.timeDescr[2], 'seconds', 59, handler);
}
footer.classList.add("cal-footer");
Object.defineProperty(this.el, "footer", { value: footer });
tryAppendChild(footer, this.el.wrapper);
}
this.setFooterContent();
}
DTBox.prototype.setFooterContent = function() {
if (this.el.footer) {
var footer = this.el.footer;
footer.hours.value = this.hours;
footer.children[0].children[1].innerText = padded(this.hours, 2);
footer.minutes.value = this.minutes;
footer.children[1].children[1].innerText = padded(this.minutes, 2);
if (this.settings.config.showSeconds) {
footer.seconds.value = this.seconds;
footer.children[2].children[1].innerText = padded(this.seconds, 2);
}
}
}
DTBox.prototype.setInputValue = function() {
var date = new Date(this.year, this.month, this.day);
var strings = [];
if (this.settings.config.showDate) {
strings.push(renderDate(date, this.settings));
}
if (this.settings.config.showTime) {
var joined = new Date(date.getTime() + this.time);
strings.push(renderTime(joined, this.settings));
}
this.elem.value = strings.join(', ');
}
DTBox.prototype.onDateSelected = function (e) {
var row = e.target.parentNode;
var date = parseInt(e.target.innerText);
if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) {
this.month += 1;
} else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) {
this.month -= 1;
}
this.day = parseInt(e.target.innerText);
this.value = new Date(this.year, this.month, this.day);
this.setInputValue();
this.setHeaderContent();
this.setBodyContent();
}
/** @param {Event} e */
DTBox.prototype.onMonthSelected = function (e) {
var col = 0;
var row = 2;
var cell = e.target;
if (cell.parentNode.nextSibling){
row = cell.parentNode.previousSibling ? 1: 0;
}
if (cell.previousSibling) {
col = 3;
if (cell.nextSibling) {
col = cell.previousSibling.previousSibling ? 2 : 1;
}
}
this.month = 4 * row + col;
this.bodyType = "DAYS";
this.setHeaderContent();
this.setupBody();
}
/** @param {Event} e */
DTBox.prototype.onYearSelected = function (e) {
this.year = parseInt(e.target.innerText);
this.bodyType = "MONTHS";
this.setHeaderContent();
this.setupBody();
}
/** @param {Event} e */
DTBox.prototype.onHeaderChange = function (e) {
var cell = e.target;
if (cell.previousSibling && cell.nextSibling) {
var idx = BODYTYPES.indexOf(this.bodyType);
if (idx < 0 || !BODYTYPES[idx + 1]) {
return;
}
this.bodyType = BODYTYPES[idx + 1];
this.setupBody();
} else {
var sign = cell.previousSibling ? 1 : -1;
switch (this.bodyType) {
case "DAYS":
this.month += sign * 1;
break;
case "MONTHS":
this.year += sign * 1;
break;
case "YEARS":
this.year += sign * 10;
}
if (this.month > 11 || this.month < 0) {
this.year += Math.floor(this.month / 11);
this.month = this.month > 11 ? 0 : 11;
}
}
this.setHeaderContent();
this.setBodyContent();
}
/**
* @param {HTMLElement} elem
* @returns {{left:number, top:number}}
*/
function getOffset(elem) {
var box = elem.getBoundingClientRect();
var left = window.pageXOffset !== undefined ? window.pageXOffset :
(document.documentElement || document.body.parentNode || document.body).scrollLeft;
var top = window.pageYOffset !== undefined ? window.pageYOffset :
(document.documentElement || document.body.parentNode || document.body).scrollTop;
return { left: box.left + left, top: box.top + top };
}
function empty(e) {
for (; e.children.length; ) e.removeChild(e.children[0]);
}
function tryAppendChild(newChild, refNode) {
try {
refNode.appendChild(newChild);
return newChild;
} catch (e) {
console.trace(e);
}
}
/** @class */
function hookFuncs() {
/** @type {Handlers} */
this._funcs = {};
}
/**
* @param {string} key
* @param {Function} func
*/
hookFuncs.prototype.add = function(key, func){
if (!this._funcs[key]){
this._funcs[key] = [];
}
this._funcs[key].push(func)
}
/**
* @param {String} key
* @returns {Function[]} handlers
*/
hookFuncs.prototype.get = function(key){
return this._funcs[key] ? this._funcs[key] : [];
}
/**
* @param {Array.<string>} arr
* @param {String} string
* @returns {Array.<string>} sorted string
*/
function sortByStringIndex(arr, string) {
return arr.sort(function(a, b){
var h = string.indexOf(a);
var l = string.indexOf(b);
var rank = 0;
if (h < l) {
rank = -1;
} else if (l < h) {
rank = 1;
} else if (a.length > b.length) {
rank = -1;
} else if (b.length > a.length) {
rank = 1;
}
return rank;
});
}
/**
* Remove keys from array that are not in format
* @param {string[]} keys
* @param {string} format
* @returns {string[]} new filtered array
*/
function filterFormatKeys(keys, format) {
var out = [];
var formatIdx = 0;
for (var i = 0; i<keys.length; i++) {
var key = keys[i];
if (format.slice(formatIdx).indexOf(key) > -1) {
formatIdx += key.length;
out.push(key);
}
}
return out;
}
/**
* @template {StringNumObj} FormatObj
* @param {string} value
* @param {string} format
* @param {FormatObj} formatObj
* @param {function(Object.<string, hookFuncs>): null} setHooks
* @returns {FormatObj} formatObj
*/
function parseData(value, format, formatObj, setHooks) {
var hooks = {
canSkip: new hookFuncs(),
updateValue: new hookFuncs(),
}
var keys = sortByStringIndex(Object.keys(formatObj), format);
var filterdKeys = filterFormatKeys(keys, format);
var vstart = 0; // value start
if (setHooks) {
setHooks(hooks);
}
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var fstart = format.indexOf(key);
var _vstart = vstart; // next value start
var val = null;
var canSkip = false;
var funcs = hooks.canSkip.get(key);
vstart = vstart || fstart;
for (var j = 0; j < funcs.length; j++) {
if (funcs[j](formatObj)){
canSkip = true;
break;
}
}
if (fstart > -1 && !canSkip) {
var sep = null;
var stop = vstart + key.length;
var fnext = -1;
var nextKeyIdx = i + 1;
_vstart += key.length; // set next value start if current key is found
// get next format token used to determine separator
while (fnext == -1 && nextKeyIdx < keys.length){
var nextKey = keys[nextKeyIdx];
nextKeyIdx += 1;
if (filterdKeys.indexOf(nextKey) === -1) {
continue;
}
fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start
}
if (fnext > -1){
sep = format.slice(stop, fnext);
if (sep) {
var _stop = value.slice(vstart).indexOf(sep);
if (_stop && _stop > -1){
stop = _stop + vstart;
_vstart = stop + sep.length;
}
}
}
val = parseInt(value.slice(vstart, stop));
var funcs = hooks.updateValue.get(key);
for (var k = 0; k < funcs.length; k++) {
val = funcs[k](val, formatObj, vstart, stop);
}
}
formatObj[key] = { index: vstart, value: val };
vstart = _vstart; // set next value start
}
return formatObj;
}
/**
* @param {String} value
* @param {DTS} settings
* @returns {Date} date object
*/
function parseDate(value, settings) {
/** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */
var formatObj = {yyyy:null, yy:null, mm:null, dd:null};
var format = ((settings.dateFormat) || '').toLowerCase();
if (!format) {
throw new TypeError('dateFormat not found (' + settings.dateFormat + ')');
}
var formatObj = parseData(value, format, formatObj, function(hooks){
hooks.canSkip.add("yy", function(data){
return data["yyyy"].value;
});
hooks.updateValue.add("yy", function(val){
return 100 * Math.floor(new Date().getFullYear() / 100) + val;
});
});
var year = formatObj["yyyy"].value || formatObj["yy"].value;
var month = formatObj["mm"].value - 1;
var date = formatObj["dd"].value;
var result = new Date(year, month, date);
return result;
}
/**
* @param {String} value
* @param {DTS} settings
* @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1
*/
function parseTime(value, settings) {
var format = ((settings.timeFormat) || '').toLowerCase();
if (!format) {
throw new TypeError('timeFormat not found (' + settings.timeFormat + ')');
}
/** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */
var formatObj = {hh:null, mm:null, ss:null, a:null};
var formatObj = parseData(value, format, formatObj, function(hooks){
hooks.updateValue.add("a", function(val, data, start, stop){
return value.slice(start, start + 2);
});
});
var hours = formatObj["hh"].value;
var minutes = formatObj["mm"].value;
var seconds = formatObj["ss"].value;
var am_pm = formatObj["a"].value;
var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm;
if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){
if (am_pm_lower == 'am' && hours == 12){
hours = 0;
} else if (am_pm_lower == 'pm') {
hours += 12;
}
}
var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
return time;
}
/**
* @param {Date} value
* @param {DTS} settings
* @returns {String} date string
*/
function renderDate(value, settings) {
var format = settings.dateFormat.toLowerCase();
var date = value.getDate();
var month = value.getMonth() + 1;
var year = value.getFullYear();
var yearShort = year % 100;
var formatObj = {
dd: date < 10 ? "0" + date : date,
mm: month < 10 ? "0" + month : month,
yyyy: year,
yy: yearShort < 10 ? "0" + yearShort : yearShort
};
var str = format.replace(settings.dateFormatRegEx, function (found) {
return formatObj[found];
});
return str;
}
/**
* @param {Date} value
* @param {DTS} settings
* @returns {String} date string
*/
function renderTime(value, settings) {
var Format = settings.timeFormat;
var format = Format.toLowerCase();
var hours = value.getHours();
var minutes = value.getMinutes();
var seconds = value.getSeconds();
var am_pm = null;
var hh_am_pm = null;
if (format.indexOf('a') > -1) {
am_pm = hours >= 12 ? 'pm' : 'am';
am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm;
hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours);
}
var formatObj = {
hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours),
mm: minutes < 10 ? "0" + minutes : minutes,
ss: seconds < 10 ? "0" + seconds : seconds,
a: am_pm,
};
var str = format.replace(settings.timeFormatRegEx, function (found) {
return formatObj[found];
});
return str;
}
/**
* checks if two dates are equal
* @param {Date} date1
* @param {Date} date2
* @returns {Boolean} true or false
*/
function isEqualDate(date1, date2) {
if (!(date1 && date2)) return false;
return (date1.getFullYear() == date2.getFullYear() &&
date1.getMonth() == date2.getMonth() &&
date1.getDate() == date2.getDate());
}
/**
* @param {Number} val
* @param {Number} pad
* @param {*} default_val
* @returns {String} padded string
*/
function padded(val, pad, default_val) {
var default_val = default_val || 0;
var valStr = '' + (parseInt(val) || default_val);
var diff = Math.max(pad, valStr.length) - valStr.length;
return ('' + default_val).repeat(diff) + valStr;
}
/**
* @template X
* @template Y
* @param {X} obj
* @param {Y} objDefaults
* @returns {X|Y} merged object
*/
function setDefaults(obj, objDefaults) {
var keys = Object.keys(objDefaults);
for (var i=0; i<keys.length; i++) {
var key = keys[i];
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
obj[key] = objDefaults[key];
}
}
return obj;
}
window.dtsel = Object.create({},{
DTS: { value: DTS },
DTObj: { value: DTBox },
fn: {
value: Object.defineProperties({}, {
empty: { value: empty },
appendAfter: {
value: function (newElem, refNode) {
refNode.parentNode.insertBefore(newElem, refNode.nextSibling);
},
},
getOffset: { value: getOffset },
parseDate: { value: parseDate },
renderDate: { value: renderDate },
parseTime: {value: parseTime},
renderTime: {value: renderTime},
setDefaults: {value: setDefaults},
}),
},
});
})();

View File

@ -0,0 +1,348 @@
require('./dtsel')
const FileUpload = require('../widgets/fileupload')
const Page = require('../api/page')
const Fileinfo = require('../widgets/fileinfo')
const common = require('../api/common')
const Editor = require('./editor')
const EditArticle = {
oninit: function(vnode) {
this.loading = false
this.showLoading = null
this.data = {
article: null,
files: [],
staff: [],
}
this.pages = [{id: null, name: 'Frontpage'}]
this.pages = this.pages.concat(Page.getFlatTree())
this.newBanner = null
this.newMedia = null
this.dateInstance = null
this.editor = null
this.fetchArticle(vnode)
},
onbeforeupdate: function(vnode) {
if (this.lastid !== m.route.param('id')) {
this.fetchArticle(vnode)
}
},
fetchArticle: function(vnode) {
this.lastid = m.route.param('id')
return this.requestArticle(
common.sendRequest({
method: 'GET',
url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
}))
},
requestArticle: function(data) {
this.error = ''
if (this.showLoading) {
clearTimeout(this.showLoading)
}
if (this.data.article) {
this.showLoading = setTimeout(() => {
this.showLoading = null
this.loading = true
m.redraw()
}, 150)
} else {
this.loading = true
}
data
.then((result) => {
this.data = result
this.data.article.publish_at = new Date(this.data.article.publish_at)
if (this.data.article.id) {
document.title = 'Editing: ' + this.data.article.name + ' - Admin NFP Moe'
this.editedPath = true
} else {
document.title = 'Create Article - Admin NFP Moe'
}
}, (err) => {
this.error = err.message
})
.then(() => {
clearTimeout(this.showLoading)
this.showLoading = null
this.loading = false
m.redraw()
})
},
updateValue: function(name, e) {
if (name === 'is_featured') {
this.data.article[name] = e.currentTarget.checked
} else {
this.data.article[name] = e.currentTarget.value
}
if (name === 'path') {
this.editedPath = true
} else if (name === 'name' && !this.editedPath) {
this.data.article.path = this.data.article.name.toLowerCase().replace(/ /g, '-')
}
},
updateParent: function(e) {
this.data.article.page_id = Number(e.currentTarget.value) || null
},
updateStaffer: function(e) {
this.data.article.admin_id = Number(e.currentTarget.value)
},
mediaUploaded: function(type, file) {
if (type === 'banner') {
this.newBanner = file
} else {
this.newMedia = file
}
},
mediaRemoved: function(type) {
this.data.article['remove_' + type] = true
this.data.article[type + '_prefix'] = null
},
save: function(vnode, e) {
e.preventDefault()
if (!this.data.article.name) {
this.error = 'Name is missing'
} else if (!this.data.article.path) {
this.error = 'Path is missing'
} else {
this.error = ''
}
if (this.error) return
let formData = new FormData()
if (this.newBanner) {
formData.append('banner', this.newBanner.file)
}
if (this.newMedia) {
formData.append('media', this.newMedia.file)
}
if (this.data.article.id) {
formData.append('id', this.data.article.id)
}
formData.append('admin_id', this.data.article.admin_id || this.data.staff[0].id)
formData.append('name', this.data.article.name)
formData.append('is_featured', this.data.article.is_featured || false)
formData.append('path', this.data.article.path)
formData.append('page_id', this.data.article.page_id || null)
formData.append('publish_at', this.dateInstance.inputElem.value.replace(', ', 'T') + 'Z')
formData.append('remove_banner', this.data.article.remove_banner ? true : false)
formData.append('remove_media', this.data.article.remove_media ? true : false)
this.loading = true
this.requestArticle(
this.editor.save()
.then(body => {
formData.append('content', JSON.stringify(body))
return common.sendRequest({
method: 'PUT',
url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
body: formData,
})
})
.then(data => {
if (!data.article.id) {
throw new Error('Something went wrong with saving, try again later')
} else if (this.lastid === 'add') {
this.lastid = data.article.id.toString()
m.route.set('/admin/articles/' + data.article.id)
}
return data
})
)
},
uploadFile: function(vnode, e) {
},
view: function(vnode) {
const showPublish = this.data.article
? this.data.article.publish_at > new Date()
: false
const bannerImage = this.data.article && this.data.article.banner_prefix
? this.data.article.banner_prefix + '_large.avif'
: null
const mediaImage = this.data.article && this.data.article.media_prefix
? this.data.article.media_prefix + '_large.avif'
: null
return [
this.loading && !this.data.article
? m('div.admin-spinner.loading-spinner')
: null,
this.data.article
? m('div.admin-wrapper', [
this.loading
? m('div.loading-spinner')
: null,
m('div.admin-actions', this.data.article.id
? [
m('span', 'Actions:'),
m(m.route.Link, { href: '/article/' + this.data.article.path }, 'View article'),
]
: null),
m('article.editarticle', [
m('header', m('h1',
(this.data.article.id ? 'Edit ' : 'Create Article ') + (this.data.article.name || '(untitled)')
)
),
m('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (this.data.article.name || '(untitled)'))),
m('div.error', {
hidden: !this.error,
onclick: () => { vnode.state.error = '' },
}, this.error),
m(FileUpload, {
height: 300,
onfile: this.mediaUploaded.bind(this, 'banner'),
ondelete: this.mediaRemoved.bind(this, 'banner'),
media: bannerImage,
}),
m(FileUpload, {
class: 'cover',
useimg: true,
onfile: this.mediaUploaded.bind(this, 'media'),
ondelete: this.mediaRemoved.bind(this, 'media'),
media: mediaImage,
}),
m('form.editarticle.content', {
onsubmit: this.save.bind(this, vnode),
}, [
m('label', 'Parent'),
m('select', {
onchange: this.updateParent.bind(this),
}, this.pages.map((item) => {
return m('option', {
value: item.id || 0,
selected: item.id === this.data.article.page_id
}, item.name)
})),
m('div.input-row', [
m('div.input-group', [
m('label', 'Name'),
m('input', {
type: 'text',
value: this.data.article.name,
oninput: this.updateValue.bind(this, 'name'),
}),
]),
m('div.input-group', [
m('label', 'Path'),
m('input', {
type: 'text',
value: this.data.article.path,
oninput: this.updateValue.bind(this, 'path'),
}),
]),
]),
m('label', 'Description'),
m(Editor, {
oncreate: (subnode) => {
this.editor = subnode.state.editor
},
contentdata: this.data.article.content,
}),
m('div.input-row', [
m('div.input-group', [
m('label', 'Published at'),
m('input', {
type: 'text',
oncreate: (div) => {
if (!this.dateInstance) {
this.dateInstance = new dtsel.DTS(div.dom, {
dateFormat: 'yyyy-mm-dd',
timeFormat: 'HH:MM:SS',
showTime: true,
})
window.temp = this.dateInstance
}
},
value: this.data.article.publish_at.toISOString().replace('T', ', ').split('.')[0],
}),
]),
m('div.input-group', [
m('label', 'Published by'),
m('select', {
onchange: this.updateStaffer.bind(this),
},
this.data.staff.map((item) => {
return m('option', {
value: item.id,
selected: item.id === this.data.article.admin_id
}, item.name)
})
),
]),
m('div.input-group.small', [
m('label', 'Make featured'),
m('input', {
type: 'checkbox',
checked: this.data.article.is_featured,
oninput: this.updateValue.bind(this, 'is_featured'),
}),
]),
]),
m('div', {
hidden: !this.data.article.name || !this.data.article.path
}, [
m('input', {
type: 'submit',
value: 'Save',
}),
showPublish
? m('button.submit', {
onclick: () => {
this.data.article.publish_at = new Date().toISOString()
}
}, 'Publish')
: null,
]),
]),
this.data.files.length
? m('files', [
m('h4', 'Files'),
this.data.files.map((file) => {
return m(Fileinfo, { file: file })
}),
])
: null,
this.data.article.id
? m('div.fileupload', [
'Add file',
m('input', {
accept: '*',
type: 'file',
onchange: this.uploadFile.bind(this, vnode),
}),
(vnode.state.loadingFile ? m('div.loading-spinner') : null),
])
: null,
]),
])
: m('div.error', {
hidden: !this.error,
onclick: () => { this.fetchArticle(vnode) },
}, this.error),,
]
},
}
module.exports = EditArticle

View File

@ -0,0 +1,47 @@
const Editor = {
oninit: function(vnode) {
this.editor = null
this.lastData = null
},
oncreate: function(vnode) {
this.editor = new window.EditorJS({
holder: vnode.dom,
inlineToolbar: ['link', 'bold', 'inlineCode', 'italic'],
tools: {
inlineCode: {
class: window.InlineCode, //<span class="inline-code"></span>
shortcut: 'CMD+SHIFT+M',
},
header: window.Header,
image: window.SimpleImage,
quote: window.Quote,
code: window.CodeTool,
list: {
class: window.List,
inlineToolbar: true,
config: {
defaultStyle: 'unordered'
}
},
delimiter: window.Delimiter,
htmlraw: window.RawTool,
},
data: vnode.attrs.contentdata,
})
this.lastData = vnode.attrs.contentdata
},
onupdate: function(vnode) {
if (this.lastData !== vnode.attrs.contentdata) {
this.lastData = vnode.attrs.contentdata
this.editor.render(this.lastData)
}
},
view: function(vnode) {
return m('div')
}
}
module.exports = Editor

View File

@ -0,0 +1,262 @@
const FileUpload = require('../widgets/fileupload')
const Page = require('../api/page.p')
const common = require('../api/common')
const Editor = require('./editor')
const EditPage = {
oninit: function(vnode) {
this.loading = false
this.showLoading = null
this.data = {
page: null,
}
this.pages = [{id: null, name: 'Frontpage'}]
this.pages = this.pages.concat(Page.getFlatTree())
this.newBanner = null
this.newMedia = null
this.editor = null
this.fetchPage(vnode)
},
onbeforeupdate: function(vnode) {
if (this.lastid !== m.route.param('id')) {
this.fetchPage(vnode)
}
},
fetchPage: function(vnode) {
this.lastid = m.route.param('id')
return this.requestPage(
common.sendRequest({
method: 'GET',
url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
}))
},
requestPage: function(data) {
this.error = ''
if (this.showLoading) {
clearTimeout(this.showLoading)
}
if (this.data.page) {
this.showLoading = setTimeout(() => {
this.showLoading = null
this.loading = true
m.redraw()
}, 150)
} else {
this.loading = true
}
data
.then((result) => {
this.data = result
if (this.data.page.id) {
document.title = 'Editing: ' + this.data.page.name + ' - Admin NFP Moe'
this.editedPath = true
} else {
document.title = 'Create Page - Admin NFP Moe'
}
}, (err) => {
this.error = err.message
})
.then(() => {
clearTimeout(this.showLoading)
this.showLoading = null
this.loading = false
m.redraw()
})
},
updateValue: function(name, e) {
this.data.page[name] = e.currentTarget.value
if (name === 'path') {
this.editedPath = true
} else if (name === 'name' && !this.editedPath) {
this.data.page.path = this.data.page.name.toLowerCase().replace(/ /g, '-')
}
},
updateParent: function(e) {
this.data.page.parent_id = Number(e.currentTarget.value) || null
},
mediaUploaded: function(type, file) {
if (type === 'banner') {
this.newBanner = file
} else {
this.newMedia = file
}
},
mediaRemoved: function(type) {
this.data.page['remove_' + type] = true
this.data.page[type + '_prefix'] = null
},
save: function(vnode, e) {
e.preventDefault()
if (!this.data.page.name) {
this.error = 'Name is missing'
} else if (!this.data.page.path) {
this.error = 'Path is missing'
} else {
this.error = ''
}
if (this.error) return
let formData = new FormData()
if (this.newBanner) {
formData.append('banner', this.newBanner.file)
}
if (this.newMedia) {
formData.append('media', this.newMedia.file)
}
if (this.data.page.id) {
formData.append('id', this.data.page.id)
}
formData.append('name', this.data.page.name)
formData.append('parent_id', this.data.page.parent_id || null)
formData.append('path', this.data.page.path)
formData.append('remove_banner', this.data.page.remove_banner ? true : false)
formData.append('remove_media', this.data.page.remove_media ? true : false)
this.loading = true
this.requestPage(
this.editor.save()
.then(body => {
formData.append('content', JSON.stringify(body))
return common.sendRequest({
method: 'PUT',
url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
body: formData,
})
})
.then(data => {
if (!data.page.id) {
throw new Error('Something went wrong with saving, try again later')
} else if (this.lastid === 'add') {
this.lastid = data.page.id.toString()
m.route.set('/admin/pages/' + data.page.id)
}
return Page.refreshTree().then(() => {
this.pages = [{id: null, name: 'Frontpage'}]
this.pages = this.pages.concat(Page.getFlatTree())
return data
})
})
)
},
view: function(vnode) {
const bannerImage = this.data.page && this.data.page.banner_prefix
? this.data.page.banner_prefix + '_large.avif'
: null
const mediaImage = this.data.page && this.data.page.media_prefix
? this.data.page.media_prefix + '_large.avif'
: null
return [
this.loading && !this.data.page
? m('div.admin-spinner.loading-spinner')
: null,
this.data.page
? m('div.admin-wrapper', [
this.loading
? m('div.loading-spinner')
: null,
m('div.admin-actions', this.data.page.id
? [
m('span', 'Actions:'),
m(m.route.Link, { href: '/page/' + this.data.page.path }, 'View page'),
]
: null),
m('article.editarticle', [
m('header', m('h1',
(this.data.page.id ? 'Edit ' : 'Create Page ') + (this.data.page.name || '(untitled)')
)
),
m('div.error', {
hidden: !this.error,
onclick: () => { vnode.state.error = '' },
}, this.error),
m(FileUpload, {
height: 300,
onfile: this.mediaUploaded.bind(this, 'banner'),
ondelete: this.mediaRemoved.bind(this, 'banner'),
media: bannerImage,
}),
m(FileUpload, {
class: 'cover',
useimg: true,
onfile: this.mediaUploaded.bind(this, 'media'),
ondelete: this.mediaRemoved.bind(this, 'media'),
media: mediaImage,
}),
m('form.editarticle.content', {
onsubmit: this.save.bind(this, vnode),
}, [
m('label', 'Parent'),
m('select', {
onchange: this.updateParent.bind(this),
}, this.pages.filter(item => !this.data.page || item.id !== this.data.page.id).map((item) => {
return m('option', {
value: item.id || 0,
selected: item.id === this.data.page.parent_id
}, item.name)
})),
m('div.input-row', [
m('div.input-group', [
m('label', 'Name'),
m('input', {
type: 'text',
value: this.data.page.name,
oninput: this.updateValue.bind(this, 'name'),
}),
]),
m('div.input-group', [
m('label', 'Path'),
m('input', {
type: 'text',
value: this.data.page.path,
oninput: this.updateValue.bind(this, 'path'),
}),
]),
]),
m('label', 'Description'),
m(Editor, {
oncreate: (subnode) => {
this.editor = subnode.state.editor
},
contentdata: this.data.page.content,
}),
m('div', {
hidden: !this.data.page.name || !this.data.page.path
}, [
m('input', {
type: 'submit',
value: 'Save',
}),
]),
]),
]),
])
: m('div.error', {
hidden: !this.error,
onclick: () => { this.fetchPage(vnode) },
}, this.error),,
]
},
}
module.exports = EditPage

View File

@ -0,0 +1,152 @@
const Staff = require('../api/staff')
const EditStaff = {
oninit: function(vnode) {
this.fetchStaff(vnode)
},
onupdate: function(vnode) {
if (this.lastid !== m.route.param('id')) {
this.fetchStaff(vnode)
}
},
fetchStaff: function(vnode) {
this.lastid = m.route.param('id')
this.loading = this.lastid !== 'add'
this.creating = this.lastid === 'add'
this.error = ''
this.staff = {
name: '',
email: '',
password: '',
rank: 10,
}
if (this.lastid !== 'add') {
Staff.getStaff(this.lastid)
.then(function(result) {
vnode.state.editedPath = true
vnode.state.staff = result
document.title = 'Editing: ' + result.name + ' - Admin NFP Moe'
})
.catch(function(err) {
vnode.state.error = err.message
})
.then(function() {
vnode.state.loading = false
m.redraw()
})
} else {
document.title = 'Creating Staff Member - Admin NFP Moe'
}
},
updateValue: function(key, e) {
this.staff[key] = e.currentTarget.value
},
save: function(vnode, e) {
e.preventDefault()
if (!this.staff.name) {
this.error = 'Fullname is missing'
} else if (!this.staff.email) {
this.error = 'Email is missing'
} else {
this.error = ''
}
if (this.error) return
this.staff.description = vnode.state.froala && vnode.state.froala.html.get() || this.staff.description
this.loading = true
let promise
if (this.staff.id) {
promise = Staff.updateStaff(this.staff.id, {
name: this.staff.name,
email: this.staff.email,
rank: this.staff.rank,
password: this.staff.password,
})
} else {
promise = Staff.createStaff({
name: this.staff.name,
email: this.staff.email,
rank: this.staff.rank,
password: this.staff.password,
})
}
promise.then(function(res) {
m.route.set('/admin/staff')
})
.catch(function(err) {
vnode.state.error = err.message
})
.then(function() {
vnode.state.loading = false
m.redraw()
})
},
updateLevel: function(e) {
this.staff.rank = Number(e.currentTarget.value)
},
view: function(vnode) {
const ranks = [[10, 'Manager'], [100, 'Admin']]
return (
this.loading ?
m('div.loading-spinner')
: m('div.admin-wrapper', [
m('div.admin-actions', this.staff.id
? [
m('span', 'Actions:'),
m(m.route.Link, { href: '/admin/staff' }, 'Staff list'),
]
: null),
m('article.editstaff', [
m('header', m('h1', this.creating ? 'Create Staff' : 'Edit ' + (this.staff.name || '(untitled)'))),
m('div.error', {
hidden: !this.error,
onclick: function() { vnode.state.error = '' },
}, this.error),
m('form.editstaff.content', {
onsubmit: this.save.bind(this, vnode),
}, [
m('label', 'Level'),
m('select', {
onchange: this.updateLevel.bind(this),
}, ranks.map(function(rank) { return m('option', { value: rank[0], selected: rank[0] === vnode.state.staff.rank }, rank[1]) })),
m('label', 'Fullname'),
m('input', {
type: 'text',
value: this.staff.name,
oninput: this.updateValue.bind(this, 'name'),
}),
m('label', 'Email'),
m('input', {
type: 'text',
value: this.staff.email,
oninput: this.updateValue.bind(this, 'email'),
}),
m('label', 'Password (optional)'),
m('input', {
type: 'text',
value: this.staff.password,
oninput: this.updateValue.bind(this, 'password'),
}),
m('input', {
type: 'submit',
value: 'Save',
}),
]),
]),
])
)
},
}
module.exports = EditStaff

View File

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

View File

@ -0,0 +1,113 @@
const Page = require('../api/page.p')
const Dialogue = require('../widgets/dialogue')
const common = require('../api/common')
const AdminPages = {
oninit: function(vnode) {
this.error = ''
this.pages = []
this.removePage = null
document.title = 'Pages - Admin NFP Moe'
this.fetchPages(vnode)
},
fetchPages: function(vnode) {
this.loading = true
this.error = ''
return common.sendRequest({
method: 'GET',
url: '/api/auth/pages',
})
.then((result) => {
this.pages = result.tree
}, (err) => {
this.error = err.message
})
.then(() => {
this.loading = false
m.redraw()
})
},
confirmRemovePage: function(vnode) {
let removingPage = this.removePage
this.removePage = null
this.loading = true
m.redraw()
return common.sendRequest({
method: 'DELETE',
url: '/api/auth/pages/' + removingPage.id,
})
.then(() => Page.refreshTree())
.then(
() => this.fetchPages(vnode),
(err) => {
this.error = err.message
this.loading = false
m.redraw()
}
)
},
drawPage: function(vnode, page) {
return [
m('tr', [
m('td', [
page.parent_id ? m('span.subpage', ' - ') : null,
m(m.route.Link, { href: '/admin/pages/' + page.id }, page.name),
]),
m('td', m(m.route.Link, { href: '/page/' + page.path }, '/page/' + page.path)),
m('td.right', page.updated_at.replace('T', ' ').split('.')[0]),
m('td.right', m('button', { onclick: function() { vnode.state.removePage = page } }, 'Remove')),
]),
].concat(page.children ? page.children.map(AdminPages.drawPage.bind(this, vnode)) : [])
},
view: function(vnode) {
return [
(this.loading ?
m('div.loading-spinner')
: m('div.admin-wrapper', [
m('div.admin-actions', [
m('span', 'Actions:'),
m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'),
]),
m('article.editpage', [
m('header', m('h1', 'All pages')),
m('div.error', {
hidden: !this.error,
onclick: () => { this.fetchPages(vnode) },
}, this.error),
m('table', [
m('thead',
m('tr', [
m('th', 'Title'),
m('th', 'Path'),
m('th.right', 'Updated'),
m('th.right', 'Actions'),
])
),
m('tbody', this.pages.map(AdminPages.drawPage.bind(this, vnode))),
]),
]),
])
),
m(Dialogue, {
hidden: vnode.state.removePage === null,
title: 'Delete ' + (vnode.state.removePage ? vnode.state.removePage.name : ''),
message: 'Are you sure you want to remove "' + (vnode.state.removePage ? vnode.state.removePage.name : '') + '" (' + (vnode.state.removePage ? vnode.state.removePage.path : '') + ')',
yes: 'Remove',
yesclass: 'alert',
no: 'Cancel',
noclass: 'cancel',
onyes: this.confirmRemovePage.bind(this, vnode),
onno: function() { vnode.state.removePage = null },
}),
]
},
}
module.exports = AdminPages

View File

@ -0,0 +1,110 @@
const Staff = require('../api/staff')
const Dialogue = require('../widgets/dialogue')
const Pages = require('../widgets/pages')
const AdminStaffList = {
oninit: function(vnode) {
this.error = ''
this.lastpage = m.route.param('page') || '1'
this.staff = []
this.removeStaff = null
this.fetchStaffs(vnode)
},
fetchStaffs: function(vnode) {
this.loading = true
document.title = 'Staff members - Admin NFP Moe'
return Staff.getAllStaff()
.then(function(result) {
vnode.state.staff = result
})
.catch(function(err) {
vnode.state.error = err.message
})
.then(function() {
vnode.state.loading = false
m.redraw()
})
},
confirmRemoveStaff: function(vnode) {
let removingStaff = this.removeStaff
this.removeStaff = null
this.loading = true
Staff.removeStaff(removingStaff.id)
.then(this.oninit.bind(this, vnode))
.catch(function(err) {
vnode.state.error = err.message
vnode.state.loading = false
m.redraw()
})
},
getLevel: function(level) {
if (level === 100) {
return 'Admin'
}
return 'Manager'
},
view: function(vnode) {
return [
m('div.admin-wrapper', [
m('div.admin-actions', [
m('span', 'Actions:'),
m(m.route.Link, { href: '/admin/staff/add' }, 'Create new staff'),
]),
m('article.editarticle', [
m('header', m('h1', 'All staff')),
m('div.error', {
hidden: !this.error,
onclick: function() { vnode.state.error = '' },
}, this.error),
(this.loading
? m('div.loading-spinner.full')
: m('table', [
m('thead',
m('tr', [
m('th', 'Fullname'),
m('th', 'Email'),
m('th', 'Level'),
m('th.right', 'Updated'),
m('th.right', 'Actions'),
])
),
m('tbody', this.staff.map(function(item) {
return m('tr', [
m('td', m(m.route.Link, { href: '/admin/staff/' + item.id }, item.name)),
m('td', item.email),
m('td.right', AdminStaffList.getLevel(item.rank)),
m('td.right', (item.updated_at || '---').replace('T', ' ').split('.')[0]),
m('td.right', m('button', { onclick: function() { vnode.state.removeStaff = item } }, 'Remove')),
])
})),
])
),
m(Pages, {
base: '/admin/staff',
links: this.links,
}),
]),
]),
m(Dialogue, {
hidden: vnode.state.removeStaff === null,
title: 'Delete ' + (vnode.state.removeStaff ? vnode.state.removeStaff.name : ''),
message: 'Are you sure you want to remove "' + (vnode.state.removeStaff ? vnode.state.removeStaff.name : '') + '" (' + (vnode.state.removeStaff ? vnode.state.removeStaff.email : '') + ')',
yes: 'Remove',
yesclass: 'alert',
no: 'Cancel',
noclass: 'cancel',
onyes: this.confirmRemoveStaff.bind(this, vnode),
onno: function() { vnode.state.removeStaff = null },
}),
]
},
}
module.exports = AdminStaffList

View File

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

View File

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

View File

@ -0,0 +1,53 @@
const Authentication = require('../authentication')
exports.sendRequest = function(options, isPagination) {
let token = Authentication.getToken()
let pagination = isPagination
if (token) {
options.headers = options.headers || {}
options.headers['Authorization'] = 'Bearer ' + token
}
options.extract = function(xhr) {
if (xhr.responseText && xhr.responseText.slice(0, 9) === '<!doctype') {
throw new Error('Expected JSON but got HTML (' + xhr.status + ': ' + this.url.split('?')[0] + ')')
}
let out = null
if (pagination && xhr.status < 300) {
let headers = {}
xhr.getAllResponseHeaders().split('\r\n').forEach(function(item) {
var splitted = item.split(': ')
headers[splitted[0]] = splitted[1]
})
out = {
headers: headers || {},
data: JSON.parse(xhr.responseText),
}
} else {
if (xhr.responseText) {
out = JSON.parse(xhr.responseText)
} else {
out = {}
}
}
if (xhr.status >= 300) {
throw out
}
return out
}
return m.request(options)
.catch(function (error) {
if (error.status === 403) {
Authentication.clearToken()
m.route.set('/login', { redirect: m.route.get() })
}
if (error.response && error.response.status) {
return Promise.reject(error.response)
}
return Promise.reject(error)
})
}

View File

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

View File

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

114
nfp_moe_old/app/api/page.js Normal file
View File

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

View File

@ -0,0 +1,54 @@
const common = require('./common')
const Tree = window.__nfptree && window.__nfptree.tree || []
const TreeMap = new Map()
exports.Tree = Tree
exports.TreeMap = TreeMap
function parseLeaf(tree) {
for (let branch of tree) {
TreeMap.set(branch.path, branch)
if (branch.children && branch.children.length) {
parseLeaf(branch.children)
}
}
}
parseLeaf(Tree)
function processPageBranch(arr, branches, prefix) {
branches.forEach((page) => {
arr.push({ id: page.id, name: prefix + page.name })
if (page.children && page.children.length) {
processPageBranch(arr, page.children, page.name + ' -> ')
}
})
}
exports.getFlatTree = function() {
let arr = []
processPageBranch(arr, Tree, '')
return arr
}
exports.getPage = function(path, page) {
return common.sendRequest({
method: 'GET',
url: '/api/' + (path ? 'pages/' + path : 'frontpage') + '?page=' + (page || 1),
})
}
exports.refreshTree = function() {
return common.sendRequest({
method: 'GET',
url: '/api/pagetree',
})
.then(pages => {
Tree.splice(0, Tree.length)
Tree.push.apply(Tree, pages.tree)
TreeMap.clear()
parseLeaf(Tree)
})
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,42 @@
const storageName = 'nfp_sites_logintoken'
const Authentication = {
currentUser: null,
isAdmin: false,
loadingListeners: [],
authListeners: [],
updateToken: function(token) {
if (!token) return Authentication.clearToken()
localStorage.setItem(storageName, token)
Authentication.currentUser = JSON.parse(atob(token.split('.')[1]))
if (Authentication.authListeners.length) {
Authentication.authListeners.forEach(function(x) { x(Authentication.currentUser) })
}
},
clearToken: function() {
Authentication.currentUser = null
localStorage.removeItem(storageName)
Authentication.isAdmin = false
},
addEvent: function(event) {
Authentication.authListeners.push(event)
},
setAdmin: function(item) {
Authentication.isAdmin = item
},
getToken: function() {
return localStorage.getItem(storageName)
},
}
Authentication.updateToken(localStorage.getItem(storageName))
window.Authentication = Authentication
module.exports = Authentication

View File

@ -0,0 +1,25 @@
const storageName = 'darkmode'
const Darkmode = {
darkIsOn: false,
setDarkMode: function(setOn) {
if (setOn) {
localStorage.setItem(storageName, true)
document.body.className = 'darkmodeon' + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly')
Darkmode.darkIsOn = true
} else {
localStorage.removeItem(storageName)
document.body.className = 'daymode' + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly')
Darkmode.darkIsOn = false
}
},
isOn: function() {
return Darkmode.darkIsOn
},
}
Darkmode.darkIsOn = localStorage.getItem(storageName)
module.exports = Darkmode

View File

@ -0,0 +1,42 @@
const m = require('mithril')
const Page = require('../api/page.p')
const Authentication = require('../authentication')
const Footer = {
oninit: function(vnode) {
this.year = new Date().getFullYear()
},
view: function() {
return [
m('div.footer-filler'),
m('div.sitemap', [
m('div', 'Sitemap'),
m(m.route.Link, { class: 'root', href: '/' }, 'Home'),
Page.Tree.map(function(page) {
return [
m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name),
(page.children
? m('ul', page.children.map(function(subpage) {
return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name))
}))
: null),
]
}),
!Authentication.currentUser
? m(m.route.Link, { class: 'root', href: '/login' }, 'Login')
: null,
m('div.meta', [
'©'
+ this.year
+ ' NFP Encodes - nfp@nfp.moe - ',
m('a', { rel: 'noopener', href: 'https://www.iubenda.com/privacy-policy/31076050', target: '_blank' }, 'Privacy Policy'),
' (Fuck EU)',
]),
]),
m('div.footer-logo'),
]
},
}
module.exports = Footer

View File

@ -0,0 +1,146 @@
const m = require('mithril')
const Page = require('../api/page.p')
const Article = require('../api/article.p')
const Pagination = require('../api/pagination')
const Pages = require('../widgets/pages')
const Newsitem = require('../widgets/newsitem')
const Frontpage = {
oninit: function(vnode) {
this.error = ''
this.loading = false
this.showLoading = null
this.data = {
page: null,
articles: [],
total_articles: 0,
featured: null,
}
this.currentPage = Number(m.route.param('page')) || 1
if (window.__nfpdata) {
this.lastpage = this.currentPage
window.__nfpdata = null
if (this.articles.length === 0) {
m.route.set('/')
}
} else {
this.fetchPage(vnode)
}
},
onbeforeupdate: function(vnode) {
this.currentPage = Number(m.route.param('page')) || 1
if (this.lastpage !== this.currentPage) {
this.fetchPage(vnode)
}
},
fetchPage: function(vnode) {
this.error = ''
this.lastpage = this.currentPage
if (this.showLoading) {
clearTimeout(this.showLoading)
}
this.showLoading = setTimeout(() => {
this.showLoading = null
this.loading = true
m.redraw()
}, 150)
if (this.lastpage !== 1) {
document.title = 'Page ' + this.lastpage + ' - NFP Moe - Anime/Manga translation group'
} else {
document.title = 'NFP Moe - Anime/Manga translation group'
}
return Page.getPage(null, this.lastpage)
.then((result) => {
this.data = result
}, (err) => {
this.error = err.message
})
.then(() => {
clearTimeout(this.showLoading)
this.showLoading = null
this.loading = false
m.redraw()
})
},
view: function(vnode) {
var deviceWidth = window.innerWidth
var bannerPath = this.data.featured && this.data.featured.banner_alt_prefix
if (bannerPath) {
var pixelRatio = window.devicePixelRatio || 1
if ((deviceWidth < 720 && pixelRatio <= 1)
|| (deviceWidth < 360 && pixelRatio <= 2)) {
bannerPath += '_small'
} else if ((deviceWidth < 1300 && pixelRatio <= 1)
|| (deviceWidth < 650 && pixelRatio <= 2)) {
bannerPath += '_medium'
} else {
bannerPath += '_large'
}
if (window.supportsavif) {
bannerPath += '.avif'
} else {
bannerPath += '.jpg'
}
}
return [
(bannerPath
? m(m.route.Link, {
class: 'frontpage-banner',
href: '/article/' + this.data.featured.path,
style: { 'background-image': 'url("' + bannerPath + '")' },
},
this.data.featured.name
)
: null),
m('frontpage', [
m('aside.sidebar', [
m('div.categories', [
m('h4', 'Categories'),
m('div',
Page.Tree.map(function(page) {
return [
m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name),
(page.children.length
? m('ul', page.children.map(function(subpage) {
return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name))
}))
: null),
]
})
),
]),
m('div.asunaside', {
class: window.supportsavif ? 'avif' : 'jpeg'
}),
]),
m('.frontpage-news', [
(this.loading
? m('div.loading-spinner')
: null),
this.data.articles.map(function(article) {
return m(Newsitem, { article: article })
}),
m(Pages, {
base: '/',
total: this.data.total_articles,
page: this.currentPage,
}),
]),
]),
]
},
}
module.exports = Frontpage

135
nfp_moe_old/app/index.js Normal file
View File

@ -0,0 +1,135 @@
require('./polyfill')
const m = require('mithril')
window.m = m
m.route.setOrig = m.route.set
m.route.set = function(path, data, options){
m.route.setOrig(path, data, options)
window.scrollTo(0, 0)
}
m.route.linkOrig = m.route.link
m.route.link = function(vnode){
m.route.linkOrig(vnode)
window.scrollTo(0, 0)
}
const Authentication = require('./authentication')
m.route.prefix = ''
window.adminRoutes = {}
let loadingAdmin = false
let loadedAdmin = false
let loaded = 0
let elements = []
const onLoaded = function() {
loaded++
if (loaded < 2) return
Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.rank >= 10)
loadedAdmin = true
m.route.set(m.route.get())
}
const onError = function(a, b, c) {
elements.forEach(function(x) { x.remove() })
loadedAdmin = loadingAdmin = false
loaded = 0
m.route.set('/logout')
}
const loadAdmin = function(user) {
if (loadingAdmin) {
if (loadedAdmin) {
Authentication.setAdmin(user && user.rank >= 10)
}
return
}
if (!user || user.rank < 10) return
loadingAdmin = true
let token = Authentication.getToken()
let element = document.createElement('link')
elements.push(element)
element.setAttribute('rel', 'stylesheet')
element.setAttribute('type', 'text/css')
element.setAttribute('href', '/assets/admin.css?token=' + token)
element.onload = onLoaded
element.onerror = onError
document.getElementsByTagName('head')[0].appendChild(element)
element = document.createElement('script')
elements.push(element)
element.setAttribute('type', 'text/javascript')
element.setAttribute('src', '/assets/admin.js?token=' + token)
element.onload = onLoaded
element.onerror = onError
document.body.appendChild(element)
element = document.createElement('script')
elements.push(element)
element.setAttribute('type', 'text/javascript')
element.setAttribute('src', '/assets/editor.js')
element.onload = onLoaded
element.onerror = onError
document.body.appendChild(element)
}
Authentication.addEvent(loadAdmin)
if (Authentication.currentUser) {
loadAdmin(Authentication.currentUser)
}
const Menu = require('./menu/menu')
const Footer = require('./footer/footer')
const Frontpage = require('./frontpage/frontpage')
const Login = require('./login/login')
const Logout = require('./login/logout')
const Page = require('./pages/page')
const Article = require('./article/article')
const menuRoot = document.getElementById('nav')
const mainRoot = document.getElementById('main')
const footerRoot = document.getElementById('footer')
const Loader = {
view: function() { return m('div.loading-spinner') },
}
const AdminResolver = {
onmatch: function(args, requestedPath) {
if (window.adminRoutes[args.path]) {
return window.adminRoutes[args.path][args.id && 1 || 0]
}
return Loader
},
render: function(vnode) { return vnode },
}
const allRoutes = {
'/': Frontpage,
'/login': Login,
'/logout': Logout,
'/page/:id': Page,
'/article/:id': Article,
'/admin/:path': AdminResolver,
'/admin/:path/:id': AdminResolver,
}
// Wait until we finish checking avif support, some views render immediately and will ask for this immediately before the callback gets called.
/*
* imgsupport.js from leechy/imgsupport
*/
const AVIF = new Image();
AVIF.onload = AVIF.onerror = function () {
window.supportsavif = (AVIF.height === 2)
document.body.className = document.body.className + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly')
m.route(mainRoot, '/', allRoutes)
m.mount(menuRoot, Menu)
m.mount(footerRoot, Footer)
}
AVIF.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=';

View File

@ -0,0 +1,101 @@
const m = require('mithril')
const Authentication = require('../authentication')
const Api = require('../api/common')
const Login = {
loading: false,
redirect: '',
error: '',
oninit: function(vnode) {
Login.redirect = vnode.attrs.redirect || ''
if (Authentication.currentUser) return m.route.set('/')
Login.error = ''
this.username = ''
this.password = ''
},
oncreate: function() {
if (Authentication.currentUser) return
},
loginuser: function(vnode, e) {
e.preventDefault()
if (!this.username) {
Login.error = 'Email is missing'
} else if (!this.password) {
Login.error = 'Password is missing'
} else {
Login.error = ''
}
if (Login.error) return
Login.loading = true
Api.sendRequest({
method: 'POST',
url: '/api/authentication/login',
body: {
email: this.username,
password: this.password,
},
})
.then(function(result) {
if (!result.token) {
return Promise.reject(new Error('Server authentication down.'))
}
Authentication.updateToken(result.token)
m.route.set(Login.redirect || '/')
})
.catch(function(error) {
Login.error = 'Error while logging into NFP! ' + error.message
vnode.state.password = ''
})
.then(function () {
Login.loading = false
m.redraw()
})
},
view: function(vnode) {
return [
m('div.login-wrapper', [
m('div.login-icon'),
m('article.login', [
m('header', [
m('h1', 'NFP.moe login'),
]),
m('div.content', [
m('h5', 'Please login to access restricted area'),
Login.error ? m('div.error', Login.error) : null,
Login.loading ? m('div.loading-spinner') : null,
m('form', {
hidden: Login.loading,
onsubmit: this.loginuser.bind(this, vnode),
}, [
m('label', 'Email'),
m('input', {
type: 'text',
value: this.username,
oninput: function(e) { vnode.state.username = e.currentTarget.value },
}),
m('label', 'Password'),
m('input', {
type: 'password',
value: this.password,
oninput: function(e) { vnode.state.password = e.currentTarget.value },
}),
m('input', {
type: 'submit',
value: 'Login',
}),
]),
]),
]),
]),
]
},
}
module.exports = Login

View File

@ -0,0 +1,15 @@
const m = require('mithril')
const Authentication = require('../authentication')
const Logout = {
oninit: function() {
Authentication.clearToken()
m.route.set('/')
},
view: function() {
return m('div.loading-spinner')
},
}
module.exports = Logout

View File

@ -0,0 +1,92 @@
const m = require('mithril')
const Authentication = require('../authentication')
const Darkmode = require('../darkmode')
const Page = require('../api/page.p')
const Menu = {
currentActive: 'home',
error: '',
loading: false,
onbeforeupdate: function() {
let currentPath = m.route.get()
if (currentPath === '/') Menu.currentActive = 'home'
else if (currentPath === '/login') Menu.currentActive = 'login'
else Menu.currentActive = currentPath
},
oninit: function(vnode) {
Menu.onbeforeupdate()
if (Page.Tree.length) return
Menu.loading = true
Page.refreshTree()
.catch(function(err) {
Menu.error = err.message
})
.then(function() {
Menu.loading = false
m.redraw()
})
},
view: function() {
console.log('menu view', Boolean(Authentication.currentUser))
return [
m('div.top', [
m(m.route.Link,
{ href: '/', class: 'logo' },
m('h2', 'NFP Moe')
),
m('aside', Authentication.currentUser ? [
m('p', [
'Welcome ' + Authentication.currentUser.name,
m(m.route.Link, { href: '/logout' }, 'Logout'),
(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.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.rank < 100, href: '/admin/staff' }, 'Staff'),
])
: (Authentication.currentUser.rank > 10 ? m('div.loading-spinner') : null)
),
] : (Darkmode.darkIsOn
? m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, false) }, 'Day mode')
: m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, true) }, 'Night mode')
)
),
]),
m('nav', [
m(m.route.Link, {
href: '/',
class: Menu.currentActive === 'home' ? 'active' : '',
}, 'Home'),
Menu.loading ? m('div.loading-spinner') : Page.Tree.map(function(page) {
if (page.children) {
return m('div.hassubmenu', [
m(m.route.Link, {
href: '/page/' + page.path,
class: Menu.currentActive === ('/page/' + page.path) ? 'active' : '',
}, page.name),
])
}
return m(m.route.Link, {
href: '/page/' + page.path,
class: Menu.currentActive === ('/page/' + page.path) ? 'active' : '',
}, page.name)
}),
]),
Menu.error ? m('div.menuerror', Menu.error) : null,
]
},
}
module.exports = Menu

View File

@ -0,0 +1,233 @@
const m = require('mithril')
const ApiPage = require('../api/page.p')
const Article = require('../api/article.p')
const pagination = require('../api/pagination')
const Authentication = require('../authentication')
const Newsentry = require('../widgets/newsentry')
const Pages = require('../widgets/pages')
const Page = {
oninit: function(vnode) {
this.error = ''
this.loading = false
this.showLoading = null
this.data = {
page: null,
articles: [],
total_articles: 0,
featured: null,
}
this.children = []
this.currentPage = Number(m.route.param('page')) || 1
if (window.__nfpdata) {
this.path = m.route.param('id')
this.data = window.__nfpdata
window.__nfpdata = null
window.__nfpsubdata = null
} else {
this.fetchPage(vnode)
}
},
onbeforeupdate: function(vnode) {
this.currentPage = Number(m.route.param('page')) || 1
if (this.path !== m.route.param('id') || this.currentPage !== this.lastpage) {
this.fetchPage(vnode)
}
},
fetchPage: function(vnode) {
this.error = ''
this.lastpage = this.currentPage
this.path = m.route.param('id')
if (this.showLoading) {
clearTimeout(this.showLoading)
}
if (this.data.page) {
this.showLoading = setTimeout(() => {
this.showLoading = null
this.loading = true
m.redraw()
}, 150)
} else {
this.loading = true
}
this.children = ApiPage.TreeMap.get(this.path)
this.children = this.children && this.children.children || []
ApiPage.getPage(this.path, this.lastpage)
.then((result) => {
this.data = result
if (!this.data.page) {
this.error = 'Page not found'
return
}
if (this.data.page.media_alt_prefix) {
this.data.page.pictureFallback = this.data.page.media_alt_prefix + '_small.jpg'
this.data.page.pictureJpeg = this.data.page.media_alt_prefix + '_small.jpg' + ' 720w, '
+ this.data.page.media_alt_prefix + '_medium.jpg' + ' 1300w, '
+ this.data.page.media_alt_prefix + '_large.jpg 1920w'
this.data.page.pictureAvif = this.data.page.media_alt_prefix + '_small.avif' + ' 720w, '
+ this.data.page.media_alt_prefix + '_medium.avif' + ' 1300w, '
+ this.data.page.media_alt_prefix + '_large.avif 1920w'
this.data.page.pictureCover = '(max-width: 840px) calc(100vw - 82px), '
+ '758px'
} else {
this.data.page.pictureFallback = null
this.data.page.pictureJpeg = null
this.data.page.pictureAvif = null
this.data.page.pictureCover = null
}
if (this.lastpage !== 1) {
document.title = 'Page ' + this.lastpage + ' - ' + this.data.page.name + ' - NFP Moe'
} else {
document.title = this.data.page.name + ' - NFP Moe'
}
}, (err) => {
this.error = err.message
})
.then(() => {
clearTimeout(this.showLoading)
this.showLoading = null
this.loading = false
m.redraw()
})
},
view: function(vnode) {
let page = this.data.page
let bannerPath = ''
return ([
this.loading
? m('article.page', m('div.loading-spinner'))
: null,
!this.loading && this.error
? m('div.error-wrapper', m('div.error', {
onclick: function() {
vnode.state.error = ''
vnode.state.fetchPage(vnode)
},
}, 'Page error: ' + this.error))
: null,
!this.loading && !this.error
? m('article.page', [
bannerPath
? m('.div.page-banner', { style: { 'background-image': 'url("' + bannerPath + '")' } } )
: null,
m('div.goback', ['« ', m(m.route.Link, {
href: page.parent_path
? '/page/' + page.parent_path
: '/'
}, page.parent_name || 'Home')]),
m('header', m('h1', page.name)),
m('.container', {
class: this.children.length ? 'multi' : '',
}, [
this.children.length
? m('aside.sidebar', [
m('h4', 'View ' + page.name + ':'),
this.children.map(function(page) {
return m(m.route.Link, { href: '/page/' + page.path }, page.name)
}),
])
: null,
page.content
? m('.fr-view', [
page.pictureFallback
? m('a.cover', {
rel: 'noopener',
href: page.media_path,
}, [
m('picture', [
m('source', {
srcset: page.pictureAvif,
sizes: page.pictureCover,
type: 'image/avif',
}),
m('img', {
srcset: page.pictureJpeg,
sizes: page.pictureCover,
alt: 'Image for news item ' + page.name,
src: page.pictureFallback,
}),
]),
])
: null,
page.content.blocks.map(block => {
return m(EditorBlock, { block: block })
}),
this.data.articles.length && page.content
? m('aside.news', [
m('h4', 'Latest posts under ' + page.name + ':'),
this.data.articles.map(function(article) {
return m(Newsentry, { article: article })
}),
m(Pages, {
base: '/page/' + page.path,
total: this.data.total_articles,
page: this.currentPage,
}),
])
: null,
])
: this.data.articles.length
? m('aside.news.single',
[
page.pictureFallback
? m('a', {
rel: 'noopener',
href: page.media_path,
}, [
m('picture.page-cover', [
m('source', {
srcset: page.pictureAvif,
sizes: page.pictureCover,
type: 'image/avif',
}),
m('img', {
srcset: page.pictureJpeg,
sizes: page.pictureCover,
alt: 'Cover image for ' + page.name,
src: page.pictureFallback,
}),
]),
])
: null,
m('h4', 'Latest posts under ' + page.name + ':'),
this.data.articles.map(function(article) {
return m(Newsentry, { article: article })
}),
m(Pages, {
base: '/page/' + page.path,
total: this.data.total_articles,
page: this.currentPage,
}),
])
: page.media
? m('img.page-cover.single', { src: page.media.medium_url, alt: 'Cover image for ' + page.name } )
: null,
]),
Authentication.currentUser
? m('div.admin-actions', [
m('span', 'Admin controls:'),
m(m.route.Link, { href: '/admin/pages/' + page.path }, 'Edit page'),
])
: null,
])
: null,
])
},
}
module.exports = Page

View File

@ -0,0 +1,8 @@
if (!String.prototype.endsWith) {
String.prototype.endsWith = function(search, this_len) {
if (this_len === undefined || this_len > this.length) {
this_len = this.length;
}
return this.substring(this_len - search.length, this_len) === search;
};
}

View File

@ -0,0 +1,17 @@
const Dialogue = {
view: function(vnode) {
return m('div.floating-container', {
hidden: vnode.attrs.hidden,
}, m('dialogue', [
m('h2', vnode.attrs.title),
m('p', vnode.attrs.message),
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),
]),
])
)
},
}
module.exports = Dialogue

View File

@ -0,0 +1,61 @@
/*
Blocks:
* Paragraph
* Header
* SimpleImage
* Quote
* CodeTool
* List
* Delimiter
* RawTool
Other:
* InlineCode
*/
const EditorBlock = {
oninit: function(vnode) {
this.id = null
this.output = null
this.onbeforeupdate(vnode)
},
onbeforeupdate: function(vnode) {
if (!vnode.attrs.block && !this.id) {
return false
}
if (vnode.attrs.block && vnode.attrs.block.id
&& vnode.attrs.block.id === this.id) {
return false
}
if (vnode.attrs.block && vnode.attrs.block.id
&& vnode.attrs.block.id !== this.id) {
this.renderblock(vnode)
} else {
this.output = null
}
},
renderblock: function(vnode) {
let block = vnode.attrs.block
this.id = block.id
switch (block.type) {
case 'paragraph':
this.output = m('p', m.trust(block.data.text))
break
case 'htmlraw':
this.output = m.trust(block.data.html)
break
default:
this.output = m('p', m.trust(block))
break
}
},
view: function(vnode) {
return this.output
}
}
module.exports = EditorBlock

View File

@ -0,0 +1,92 @@
const Fileinfo = {
getPrefix: function(vnode) {
if (!vnode.attrs.file.filename.endsWith('.torrent')) {
return vnode.attrs.file.filename.split('.').slice(-1)
}
if (vnode.attrs.file.filename.indexOf('720 ') >= 0) {
return '720p'
}
if (vnode.attrs.file.filename.indexOf('1080 ') >= 0) {
return '1080p'
}
if (vnode.attrs.file.filename.indexOf('480 ') >= 0) {
return '480p'
}
if (vnode.attrs.file.filename.toLowerCase().indexOf('flac') >= 0) {
return 'FLAC'
}
if (vnode.attrs.file.filename.toLowerCase().indexOf('mp3') >= 0) {
return 'MP3'
}
if (vnode.attrs.file.filename.toLowerCase().indexOf('psd') >= 0) {
return 'PSD'
}
if (vnode.attrs.file.filename.toLowerCase().indexOf('.zip') >= 0) {
return 'ZIP'
}
return 'Other'
},
getTitle: function(vnode) {
if (vnode.attrs.file.meta.torrent) {
return vnode.attrs.file.meta.torrent.name
}
return vnode.attrs.file.filename
},
getDownloadName: function(vnode) {
if (vnode.attrs.file.meta.torrent) {
return 'Torrent'
}
return 'Download'
},
getSize: function(orgSize) {
var size = orgSize
var i = -1
var byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']
do {
size = size / 1024
i++
} while (size > 1024)
return Math.max(size, 0.1).toFixed(1) + byteUnits[i]
},
view: function(vnode) {
return m('fileinfo', { class: vnode.attrs.slim ? 'slim' : ''}, [
m('div.filetitle', [
m('span.prefix', this.getPrefix(vnode) + ':'),
m('a', {
target: '_blank',
rel: 'noopener',
href: vnode.attrs.file.url,
}, this.getDownloadName(vnode)),
vnode.attrs.file.magnet
? m('a', {
href: vnode.attrs.file.magnet,
}, 'Magnet')
: null,
m('span', this.getTitle(vnode)),
]),
vnode.attrs.file.meta.torrent
&& !vnode.attrs.slim
&& vnode.attrs.file.meta.torrent.files.length > 1
&& (!vnode.attrs.trim || vnode.attrs.file.meta.torrent.files.length <= 4)
? m('ul', vnode.attrs.file.meta.torrent.files.map(function(file) {
return m('li', [
file.name + ' ',
m('span.meta', '(' + Fileinfo.getSize(file.size) + ')'),
])
}))
: null,
vnode.attrs.trim
&& vnode.attrs.file.meta.torrent
&& vnode.attrs.file.meta.torrent.files.length > 4
? m('div.trimmed', '...' + vnode.attrs.file.meta.torrent.files.length + ' files...')
: null,
])
},
}
module.exports = Fileinfo

View File

@ -0,0 +1,64 @@
const FileUpload = {
fileChanged: function(vnode, event) {
if (!event.target.files[0]) return
let preview = null
if (event.target.files[0].type.startsWith('image')) {
preview = URL.createObjectURL(event.target.files[0])
}
if (this.preview) {
this.preview.clear()
}
let out = {
file: event.target.files[0],
preview: preview,
clear: function() {
URL.revokeObjectURL(preview)
}
}
this.preview = out
vnode.attrs.onfile(out)
},
oninit: function(vnode) {
this.loading = false
this.preview = null
},
view: function(vnode) {
let media = vnode.attrs.media
return m('fileupload', {
class: vnode.attrs.class || null,
}, [
this.preview || media
? vnode.attrs.useimg
? [ m('img', { src: this.preview && this.preview.preview || media }), m('div.showicon')]
: m('a.display.inside', {
href: this.preview && this.preview.preview || media,
style: {
'background-image': 'url("' + (this.preview && this.preview.preview || media) + '")',
},
}, m('div.showicon'))
: m('div.inside.showbordericon')
,
m('input', {
accept: 'image/*',
type: 'file',
onchange: this.fileChanged.bind(this, vnode),
}),
media && vnode.attrs.ondelete
? m('button.remove', { onclick: vnode.attrs.ondelete })
: null,
this.loading
? m('div.loading-spinner')
: null,
])
},
}
module.exports = FileUpload

View File

@ -0,0 +1,99 @@
const Fileinfo = require('./fileinfo')
const Newsentry = {
oninit: function(vnode) {
this.lastId = null
this.onbeforeupdate(vnode)
},
strip: function(html) {
var doc = new DOMParser().parseFromString(html, 'text/html')
var out = doc.body.textContent || ''
var splitted = out.split('.')
if (splitted.length > 2) {
return splitted.slice(0, 2).join('.') + '...'
}
return out
},
onbeforeupdate: function(vnode) {
let article = vnode.attrs.article
if (this.lastId !== article.id) {
this.lastId = article.id
this.description = null
for (let i = 0; i < article.content.blocks.length; i++) {
if (article.content.blocks[i].type === 'paragraph') {
this.description = article.content.blocks[i].data.text
break
} else if (article.content.blocks[i].type === 'htmlraw') {
this.description = this.strip(article.content.blocks[i].data.html)
break
}
}
if (article.media_alt_prefix) {
this.pictureFallback = article.media_alt_prefix + '_small.jpg'
this.pictureJpeg = article.media_alt_prefix + '_small.jpg' + ' 720w, '
+ article.media_alt_prefix + '_medium.jpg' + ' 1300w, '
+ article.media_alt_prefix + '_large.jpg 1920w'
this.pictureAvif = article.media_alt_prefix + '_small.avif' + ' 720w, '
+ article.media_alt_prefix + '_medium.avif' + ' 1300w, '
+ article.media_alt_prefix + '_large.avif 1920w'
this.pictureCover = '(max-width: 440px) calc(100vw - 40px), '
+ '124px'
} else {
this.pictureFallback = null
this.pictureJpeg = null
this.pictureAvif = null
this.pictureCover = null
}
}
},
view: function(vnode) {
let article = vnode.attrs.article
return m('newsentry', [
this.pictureFallback
? m(m.route.Link, {
class: 'cover',
href: '/article/' + article.path,
},
m('picture', [
m('source', {
srcset: this.pictureAvif,
sizes: this.pictureCover,
type: 'image/avif',
}),
m('img', {
srcset: this.pictureJpeg,
sizes: this.pictureCover,
alt: 'Image for news item ' + article.name,
src: this.pictureFallback,
}),
])
)
: m('a.cover.nobg'),
m('div.entrycontent', [
m('div.title', [
m(m.route.Link,
{ href: '/article/' + article.path },
m('h3', [article.name])
),
]),
(article.files && article.files.length
? article.files.map(function(file) {
return m(Fileinfo, { file: file, slim: true })
})
: this.description
? m('span.entrydescription', this.description)
: null),
]),
])
},
}
module.exports = Newsentry

View File

@ -0,0 +1,88 @@
const Fileinfo = require('./fileinfo')
const EditorBlock = require('./editorblock')
const Newsitem = {
oninit: function(vnode) {
this.lastId = null
this.onbeforeupdate(vnode)
},
onbeforeupdate: function(vnode) {
let article = vnode.attrs.article
if (this.lastId !== article.id) {
this.lastId = article.id
if (article.media_alt_prefix) {
this.pictureFallback = article.media_alt_prefix + '_small.jpg'
this.pictureJpeg = article.media_alt_prefix + '_small.jpg' + ' 720w, '
+ article.media_alt_prefix + '_medium.jpg' + ' 1300w, '
+ article.media_alt_prefix + '_large.jpg 1920w'
this.pictureAvif = article.media_alt_prefix + '_small.avif' + ' 720w, '
+ article.media_alt_prefix + '_medium.avif' + ' 1300w, '
+ article.media_alt_prefix + '_large.avif 1920w'
this.pictureCover = '(max-width: 639px) calc(100vw - 40px), '
+ '(max-width: 1000px) 300px, '
+ '400px'
} else {
this.pictureFallback = null
this.pictureJpeg = null
this.pictureAvif = null
this.pictureCover = null
}
}
},
view: function(vnode) {
let article = vnode.attrs.article
return m('newsitem', [
m(m.route.Link,
{ href: '/article/' + article.path, class: 'title' },
m('h3', [article.name])
),
m('div.newsitemcontent', [
this.pictureFallback
? m(m.route.Link, {
class: 'cover',
href: '/article/' + article.path,
},
m('picture', [
m('source', {
srcset: this.pictureAvif,
sizes: this.pictureCover,
type: 'image/avif',
}),
m('img', {
srcset: this.pictureJpeg,
sizes: this.pictureCover,
alt: 'Image for news item ' + article.name,
src: this.pictureFallback,
}),
])
)
: null,
m('div.entrycontent', [
article.content.blocks.map(block => {
return m(EditorBlock, { block: block })
}),
(article.files && article.files.length
? article.files.map(function(file) {
return m(Fileinfo, { file: file, trim: true })
})
: null),
m('span.entrymeta', [
'Posted ',
(article.page_path ? 'in' : ''),
(article.page_path ? m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name) : null),
'at ' + (article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16),
' by ' + (article.admin_name || 'Admin'),
]),
]),
]),
])
},
}
module.exports = Newsitem

View File

@ -0,0 +1,44 @@
const Pages = {
oninit: function(vnode) {
this.onbeforeupdate(vnode)
},
onbeforeupdate: function(vnode) {
this.total = vnode.attrs.total
this.currentPage = vnode.attrs.page
this.perPage = vnode.attrs.perPage || 10
this.maxPage = this.total / this.perPage + 1
},
view: function(vnode) {
if (this.total <= this.perPage) return null
return m('pages', [
this.currentPage > 1
? [
m(m.route.Link, {
href: vnode.attrs.base,
}, 'First'),
m(m.route.Link, {
href: vnode.attrs.base + (this.currentPage > 2
? '?page=' + (this.currentPage - 1)
: ''
),
}, 'Previous'),
]
: m('div'),
m('div', 'Page ' + this.currentPage),
this.currentPage < this.maxPage
? [
m(m.route.Link, {
href: vnode.attrs.base + '?page=' + (this.currentPage + 1),
}, 'Next'),
m(m.route.Link, {
href: vnode.attrs.base + '?page=' + this.maxPage,
}, 'Last')
]
: m('div'),
])
},
}
module.exports = Pages

1
nfp_moe_old/base Symbolic link
View File

@ -0,0 +1 @@
../base

22
nfp_moe_old/dev.mjs Normal file
View File

@ -0,0 +1,22 @@
import fs from 'fs'
import { ServiceCore } from 'service-core'
import * as index from './index.mjs'
var core = new ServiceCore('nfp_moe', import.meta.url, 4030, '')
let config = {
frontend: {
url: 'http://localhost:4030'
}
}
try {
config = JSON.parse(fs.readFileSync('./config.json'))
} catch {}
config.port = 4030
core.setConfig(config)
core.init(index).then(function() {
return core.run()
})

11
nfp_moe_old/index.mjs Normal file
View File

@ -0,0 +1,11 @@
import config from '../base/config.mjs'
export function start(http, port, ctx) {
config.stores.overrides.store = ctx.config
return import('./api/server.mjs')
.then(function(module) {
let server = new module.default(http, port, ctx)
return server.run()
})
}

69
nfp_moe_old/package.json Normal file
View File

@ -0,0 +1,69 @@
{
"name": "nfp_moe",
"version": "2.0.0",
"description": "NFP Moe website",
"main": "index.js",
"directories": {
"test": "test"
},
"scripts": {
"start": "node --experimental-modules index.mjs",
"test": "echo \"Error: no test specified\" && exit 1",
"build:prod": "sass -s compressed app/app.scss public/assets/app.css && sass -s compressed app/admin.scss public/assets/admin.css && asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js",
"build": "sass app/app.scss public/assets/app.css && sass app/admin.scss public/assets/admin.css && asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js",
"dev:build": "npm-watch build",
"dev:server": "node dev.mjs | bunyan",
"dev": "npm-watch dev:server",
"watch:sass:public": "sass --watch app/app.scss public/assets/app.css",
"watch:sass:admin": "sass --watch app/admin.scss public/assets/admin.css",
"prod": "npm run build && npm start",
"temp": "asbundle"
},
"watch": {
"dev:server": {
"patterns": [
"api/*",
"base/*",
"../base/*"
],
"extensions": "js,mjs",
"quiet": true,
"inherit": true
},
"build": {
"patterns": [
"app/*"
],
"extensions": "js,mjs,scss",
"quiet": true,
"inherit": true
}
},
"repository": {
"type": "git",
"url": "https://github.com/nfp-projects/nfp_moe.git"
},
"author": "Jonatan Nilsson",
"license": "WTFPL",
"bugs": {
"url": "https://github.com/nfp-projects/nfp_moe/issues"
},
"homepage": "https://github.com/nfp-projects/nfp_moe",
"dependencies": {
"@editorjs/quote": "^2.4.0",
"bencode": "^2.0.3",
"dot": "^2.0.0-beta.1",
"flaska": "^1.3.0",
"format-link-header": "^2.1.0",
"formidable": "^1.2.6",
"msnodesqlv8": "^2.4.7",
"nconf-lite": "^1.0.1",
"striptags": "^3.1.1"
},
"devDependencies": {
"asbundle": "^2.6.1",
"mithril": "^2.2.2",
"sass": "^1.52.3",
"service-core": "^3.0.0-beta.17"
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,200 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url("Inter-Thin.woff2?v=3.19") format("woff2"),
url("Inter-Thin.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url("Inter-ThinItalic.woff2?v=3.19") format("woff2"),
url("Inter-ThinItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLight.woff2?v=3.19") format("woff2"),
url("Inter-ExtraLight.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLightItalic.woff2?v=3.19") format("woff2"),
url("Inter-ExtraLightItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("Inter-Light.woff2?v=3.19") format("woff2"),
url("Inter-Light.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url("Inter-LightItalic.woff2?v=3.19") format("woff2"),
url("Inter-LightItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("Inter-Regular.woff2?v=3.19") format("woff2"),
url("Inter-Regular.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("Inter-Italic.woff2?v=3.19") format("woff2"),
url("Inter-Italic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("Inter-Medium.woff2?v=3.19") format("woff2"),
url("Inter-Medium.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url("Inter-MediumItalic.woff2?v=3.19") format("woff2"),
url("Inter-MediumItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBold.woff2?v=3.19") format("woff2"),
url("Inter-SemiBold.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBoldItalic.woff2?v=3.19") format("woff2"),
url("Inter-SemiBoldItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("Inter-Bold.woff2?v=3.19") format("woff2"),
url("Inter-Bold.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("Inter-BoldItalic.woff2?v=3.19") format("woff2"),
url("Inter-BoldItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBold.woff2?v=3.19") format("woff2"),
url("Inter-ExtraBold.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBoldItalic.woff2?v=3.19") format("woff2"),
url("Inter-ExtraBoldItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url("Inter-Black.woff2?v=3.19") format("woff2"),
url("Inter-Black.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url("Inter-BlackItalic.woff2?v=3.19") format("woff2"),
url("Inter-BlackItalic.woff?v=3.19") format("woff");
}
/* -------------------------------------------------------
Variable font.
Usage:
html { font-family: 'Inter', sans-serif; }
@supports (font-variation-settings: normal) {
html { font-family: 'Inter var', sans-serif; }
}
*/
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: normal;
font-named-instance: 'Regular';
src: url("Inter-roman.var.woff2?v=3.19") format("woff2");
}
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: italic;
font-named-instance: 'Italic';
src: url("Inter-italic.var.woff2?v=3.19") format("woff2");
}
/* --------------------------------------------------------------------------
[EXPERIMENTAL] Multi-axis, single variable font.
Slant axis is not yet widely supported (as of February 2019) and thus this
multi-axis single variable font is opt-in rather than the default.
When using this, you will probably need to set font-variation-settings
explicitly, e.g.
* { font-variation-settings: "slnt" 0deg }
.italic { font-variation-settings: "slnt" 10deg }
*/
@font-face {
font-family: 'Inter var experimental';
font-weight: 100 900;
font-display: swap;
font-style: oblique 0deg 10deg;
src: url("Inter.var.woff2?v=3.19") format("woff2");
}

View File

@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{=headerTitle}}</title>
<base href="/">
<meta name="description" content="{{=headerDescription}}">
<meta name="twitter:card" value="summary">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:type" content="website" />
<meta property="og:url" content="{{=headerUrl}}" />
<meta property="og:image" content="{{=headerImage}}" />
<meta property="og:description" content="{{=headerDescription}}" />
<meta property="og:title" content="{{=headerTitle}}" />
{{? headerImage === '/assets/img/heart.jpg' }}
<meta id="ogimagewidth" property="og:image:width" content="400" />
<meta id="ogimageheight" property="og:image:height" content="500" />
{{? }}
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
<link rel="Stylesheet" href="/assets/app.css?v={{=version}}" type="text/css" />
<link rel="preconnect" href="https://cdn.nfp.is" />
</head>
<body class="daymode">
<script type="text/javascript" nonce="{{=nonce}}">
if (localStorage.getItem('darkmode')) {document.body.className = 'darkmodeon';}
window.__nfptree = {{=payloadTree}};
window.__nfpdata = {{=payloadData}};
window.__nfplinks = {{=payloadLinks}};
</script>
<div class="maincontainer">
<div id="nav"></div>
<main id="main"></main>
<footer id="footer"></footer>
</div>
<script type="text/javascript" src="/assets/app.js?v={{=version}}"></script>
</body>
</html>

View File

@ -0,0 +1,2 @@
User-agent: *
Allow: /