Many updates, made server smarter by giving data and added meta tags
This commit is contained in:
parent
0f8cd7ac1a
commit
5b0e9d1d2d
29 changed files with 381 additions and 153 deletions
|
@ -22,7 +22,7 @@
|
||||||
"require-await": 0,
|
"require-await": 0,
|
||||||
"array-callback-return": 2,
|
"array-callback-return": 2,
|
||||||
"block-scoped-var": 2,
|
"block-scoped-var": 2,
|
||||||
"complexity": ["error", 20],
|
"complexity": ["error", 30],
|
||||||
"eqeqeq": [2, "smart"],
|
"eqeqeq": [2, "smart"],
|
||||||
"no-else-return": ["error", { "allowElseIf": false }],
|
"no-else-return": ["error", { "allowElseIf": false }],
|
||||||
"no-extra-bind": 2,
|
"no-extra-bind": 2,
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function accessChecks(opts = { }) {
|
||||||
|
|
||||||
export function restrict(level = orgAccess.Normal) {
|
export function restrict(level = orgAccess.Normal) {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
if (!ctx.headers.authorization) {
|
if (!ctx.headers.authorization && !ctx.query.token) {
|
||||||
return ctx.throw(403, 'Authentication token was not found (did you forget to login?)')
|
return ctx.throw(403, 'Authentication token was not found (did you forget to login?)')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ export function restrict(level = orgAccess.Normal) {
|
||||||
return ctx.throw(403, 'You do not have enough access to access this resource')
|
return ctx.throw(403, 'You do not have enough access to access this resource')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (next) {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,13 +77,13 @@ const Article = bookshelf.createModel({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getFrontpageArticles() {
|
getFrontpageArticles(page = 1) {
|
||||||
return this.query(qb => {
|
return this.query(qb => {
|
||||||
qb.orderBy('updated_at', 'DESC')
|
qb.orderBy('updated_at', 'DESC')
|
||||||
})
|
})
|
||||||
.fetchPage({
|
.fetchPage({
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
page: 1,
|
page: page,
|
||||||
withRelated: ['files', 'media', 'banner'],
|
withRelated: ['files', 'media', 'banner'],
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,6 +52,15 @@ export default class Jwt {
|
||||||
|
|
||||||
static jwtMiddleware() {
|
static jwtMiddleware() {
|
||||||
return koaJwt({
|
return koaJwt({
|
||||||
|
getToken: ctx => {
|
||||||
|
if (ctx.request.header.authorization) {
|
||||||
|
return ctx.request.header.authorization.split(' ')[1]
|
||||||
|
}
|
||||||
|
if (ctx.query.token) {
|
||||||
|
return ctx.query.token
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
secret: (header, payload) =>
|
secret: (header, payload) =>
|
||||||
`${config.get('jwt:secret')}${payload.email}`,
|
`${config.get('jwt:secret')}${payload.email}`,
|
||||||
passthrough: true,
|
passthrough: true,
|
||||||
|
|
|
@ -24,12 +24,12 @@ export default class Resizer {
|
||||||
.then(() => output)
|
.then(() => output)
|
||||||
}
|
}
|
||||||
|
|
||||||
createMedium(input) {
|
createMedium(input, height) {
|
||||||
let output = this.Media.getSubUrl(input, 'medium')
|
let output = this.Media.getSubUrl(input, 'medium')
|
||||||
|
|
||||||
return this.sharp(input)
|
return this.sharp(input)
|
||||||
.resize(700, 700, {
|
.resize(700, height || 700, {
|
||||||
fit: sharp.fit.inside,
|
fit: height && sharp.fit.cover || sharp.fit.inside,
|
||||||
withoutEnlargement: true,
|
withoutEnlargement: true,
|
||||||
})
|
})
|
||||||
.jpeg({
|
.jpeg({
|
||||||
|
|
|
@ -19,8 +19,13 @@ export default class MediaRoutes {
|
||||||
async upload(ctx) {
|
async upload(ctx) {
|
||||||
let result = await this.multer.processBody(ctx)
|
let result = await this.multer.processBody(ctx)
|
||||||
|
|
||||||
|
let height = null
|
||||||
|
if (ctx.query.height) {
|
||||||
|
height = Number(ctx.query.height)
|
||||||
|
}
|
||||||
|
|
||||||
let smallPath = await this.resize.createSmall(result.path)
|
let smallPath = await this.resize.createSmall(result.path)
|
||||||
let mediumPath = await this.resize.createMedium(result.path)
|
let mediumPath = await this.resize.createMedium(result.path, height)
|
||||||
let largePath = await this.resize.createLarge(result.path)
|
let largePath = await this.resize.createLarge(result.path)
|
||||||
|
|
||||||
let token = this.jwt.signDirect({ site: config.get('upload:name') }, config.get('upload:secret'))
|
let token = this.jwt.signDirect({ site: config.get('upload:name') }, config.get('upload:secret'))
|
||||||
|
|
|
@ -1,87 +1,15 @@
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
import send from 'koa-send'
|
import send from 'koa-send'
|
||||||
import dot from 'dot'
|
|
||||||
import defaults from './defaults.mjs'
|
import defaults from './defaults.mjs'
|
||||||
import config from './config.mjs'
|
import access from './access/index.mjs'
|
||||||
import Page from './page/model.mjs'
|
import { restrict } from './access/middleware.mjs'
|
||||||
import Article from './article/model.mjs'
|
import { serveIndex } from './serveindex.mjs'
|
||||||
|
|
||||||
const body = readFileSync('./public/index.html').toString()
|
const restrictAdmin = restrict(access.Manager)
|
||||||
const bodyTemplate = dot.template(body)
|
|
||||||
|
|
||||||
async function sendIndex(ctx, path) {
|
|
||||||
let tree = null
|
|
||||||
let data = null
|
|
||||||
let links = null
|
|
||||||
try {
|
|
||||||
tree = (await Page.getTree()).toJSON()
|
|
||||||
tree.forEach(item => (
|
|
||||||
item.children = item.children.map(x => (
|
|
||||||
{ id: x.id, name: x.name, path: x.path }
|
|
||||||
))
|
|
||||||
))
|
|
||||||
if (path === '/') {
|
|
||||||
data = await Article.getFrontpageArticles()
|
|
||||||
|
|
||||||
if (data.pagination.rowCount > 10) {
|
|
||||||
links = {
|
|
||||||
current: { title: 'Page 1' },
|
|
||||||
next: { page: 2, title: 'Next' },
|
|
||||||
last: { page: Math.ceil(data.pagination.rowCount / 10), title: 'Last' },
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
links = {
|
|
||||||
current: { title: 'Page 1' },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = data.toJSON().map(x => ({
|
|
||||||
id: x.id,
|
|
||||||
created_at: x.created_at,
|
|
||||||
path: x.path,
|
|
||||||
description: x.description,
|
|
||||||
name: x.name,
|
|
||||||
media: x.media && ({
|
|
||||||
medium_url: x.media.medium_url,
|
|
||||||
small_url: x.media.small_url,
|
|
||||||
}) || null,
|
|
||||||
banner: x.banner && ({
|
|
||||||
large_url: x.banner.large_url,
|
|
||||||
medium_url: x.banner.medium_url,
|
|
||||||
small_url: x.banner.small_url,
|
|
||||||
}) || null,
|
|
||||||
files: x.files && x.files.map(f => ({
|
|
||||||
filename: f.filename,
|
|
||||||
url: f.url,
|
|
||||||
magnet: f.magnet,
|
|
||||||
meta: f.meta.torrent && ({
|
|
||||||
torrent: {
|
|
||||||
files: f.meta.torrent.files.map(tf => ({
|
|
||||||
name: tf.name,
|
|
||||||
size: tf.size,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}) || {},
|
|
||||||
})) || [],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
ctx.log.error(e)
|
|
||||||
}
|
|
||||||
ctx.body = bodyTemplate({
|
|
||||||
v: config.get('CIRCLECI_VERSION'),
|
|
||||||
tree: JSON.stringify(tree),
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
links: JSON.stringify(links),
|
|
||||||
})
|
|
||||||
ctx.set('Content-Length', Buffer.byteLength(ctx.body))
|
|
||||||
ctx.set('Cache-Control', 'max-age=0')
|
|
||||||
ctx.set('Content-Type', 'text/html; charset=utf-8')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serve(docRoot, pathname, options = {}) {
|
export function serve(docRoot, pathname, options = {}) {
|
||||||
options.root = docRoot
|
options.root = docRoot
|
||||||
|
|
||||||
return (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
let opts = defaults({}, options)
|
let opts = defaults({}, options)
|
||||||
if (ctx.request.method === 'OPTIONS') return
|
if (ctx.request.method === 'OPTIONS') return
|
||||||
|
|
||||||
|
@ -96,16 +24,25 @@ export function serve(docRoot, pathname, options = {}) {
|
||||||
|| filepath.endsWith('.js')
|
|| filepath.endsWith('.js')
|
||||||
|| filepath.endsWith('.css')
|
|| filepath.endsWith('.css')
|
||||||
|| filepath.endsWith('.svg')) {
|
|| filepath.endsWith('.svg')) {
|
||||||
|
if (filepath.indexOf('admin') === -1) {
|
||||||
opts = defaults({ maxage: 2592000 * 1000 }, opts)
|
opts = defaults({ maxage: 2592000 * 1000 }, opts)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (filepath === '/index.html') {
|
if (filepath === '/index.html') {
|
||||||
return sendIndex(ctx, '/')
|
return serveIndex(ctx, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filepath.indexOf('admin') >= 0
|
||||||
|
&& (filepath.indexOf('js') >= 0
|
||||||
|
|| filepath.indexOf('css') >= 0)) {
|
||||||
|
await restrictAdmin(ctx)
|
||||||
|
ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||||
}
|
}
|
||||||
|
|
||||||
return send(ctx, filepath, opts).catch((er) => {
|
return send(ctx, filepath, opts).catch((er) => {
|
||||||
if (er.code === 'ENOENT' && er.status === 404) {
|
if (er.code === 'ENOENT' && er.status === 404) {
|
||||||
return sendIndex(ctx)
|
return serveIndex(ctx, filepath)
|
||||||
// return send(ctx, '/index.html', options)
|
// return send(ctx, '/index.html', options)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
149
api/serveindex.mjs
Normal file
149
api/serveindex.mjs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import dot from 'dot'
|
||||||
|
import striptags from 'striptags'
|
||||||
|
|
||||||
|
import config from './config.mjs'
|
||||||
|
import Page from './page/model.mjs'
|
||||||
|
import Article from './article/model.mjs'
|
||||||
|
|
||||||
|
const body = readFileSync('./public/index.html').toString()
|
||||||
|
const bodyTemplate = dot.template(body)
|
||||||
|
|
||||||
|
function mapArticle(x) {
|
||||||
|
return {
|
||||||
|
id: x.id,
|
||||||
|
created_at: x.created_at,
|
||||||
|
path: x.path,
|
||||||
|
description: x.description,
|
||||||
|
name: x.name,
|
||||||
|
media: x.media && ({
|
||||||
|
large_url: x.media.large_url,
|
||||||
|
medium_url: x.media.medium_url,
|
||||||
|
small_url: x.media.small_url,
|
||||||
|
}) || null,
|
||||||
|
banner: x.banner && ({
|
||||||
|
large_url: x.banner.large_url,
|
||||||
|
medium_url: x.banner.medium_url,
|
||||||
|
small_url: x.banner.small_url,
|
||||||
|
}) || null,
|
||||||
|
parent: x.parent && ({
|
||||||
|
id: x.parent.id,
|
||||||
|
name: x.parent.name,
|
||||||
|
path: x.parent.path,
|
||||||
|
}),
|
||||||
|
files: x.files && x.files.map(f => ({
|
||||||
|
filename: f.filename,
|
||||||
|
url: f.url,
|
||||||
|
magnet: f.magnet,
|
||||||
|
meta: f.meta.torrent && ({
|
||||||
|
torrent: {
|
||||||
|
files: f.meta.torrent.files.map(tf => ({
|
||||||
|
name: tf.name,
|
||||||
|
size: tf.size,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}) || {},
|
||||||
|
})) || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapPage(x) {
|
||||||
|
return {
|
||||||
|
id: x.id,
|
||||||
|
created_at: x.created_at,
|
||||||
|
path: x.path,
|
||||||
|
description: x.description,
|
||||||
|
name: x.name,
|
||||||
|
media: x.media && ({
|
||||||
|
large_url: x.media.large_url,
|
||||||
|
medium_url: x.media.medium_url,
|
||||||
|
small_url: x.media.small_url,
|
||||||
|
}) || null,
|
||||||
|
banner: x.banner && ({
|
||||||
|
large_url: x.banner.large_url,
|
||||||
|
medium_url: x.banner.medium_url,
|
||||||
|
small_url: x.banner.small_url,
|
||||||
|
}) || null,
|
||||||
|
children: x.children && x.children.map(f => ({
|
||||||
|
id: f.id,
|
||||||
|
path: f.path,
|
||||||
|
name: f.name,
|
||||||
|
})) || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function serveIndex(ctx, path) {
|
||||||
|
let tree = null
|
||||||
|
let data = null
|
||||||
|
let links = null
|
||||||
|
let image = '/assets/img/heart.jpg'
|
||||||
|
let title = 'NFP Moe - Anime/Manga translation group'
|
||||||
|
let description = 'Small fansubbing and scanlation group translating and encoding our favourite shows from Japan.'
|
||||||
|
try {
|
||||||
|
tree = (await Page.getTree()).toJSON()
|
||||||
|
tree.forEach(item => (
|
||||||
|
item.children = item.children.map(x => (
|
||||||
|
{ id: x.id, name: x.name, path: x.path }
|
||||||
|
))
|
||||||
|
))
|
||||||
|
if (path === '/') {
|
||||||
|
data = await Article.getFrontpageArticles(Number(ctx.query.page || '1'))
|
||||||
|
|
||||||
|
if (data.pagination.rowCount > 10) {
|
||||||
|
links = {
|
||||||
|
current: { title: 'Page 1' },
|
||||||
|
next: { page: 2, title: 'Next' },
|
||||||
|
last: { page: Math.ceil(data.pagination.rowCount / 10), title: 'Last' },
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
links = {
|
||||||
|
current: { title: 'Page 1' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = data.toJSON().map(mapArticle)
|
||||||
|
} else if (path.startsWith('/article/') || path.startsWith('/page/')) {
|
||||||
|
let id = path.split('/')[2]
|
||||||
|
if (id) {
|
||||||
|
let found
|
||||||
|
if (path.startsWith('/article/')) {
|
||||||
|
found = await Article.getSingle(id, ['media', 'parent', 'banner', 'files'])
|
||||||
|
found = mapArticle(found.toJSON())
|
||||||
|
data = found
|
||||||
|
} else {
|
||||||
|
found = await Page.getSingle(id, ['media', 'banner', 'children'])
|
||||||
|
found = mapPage(found.toJSON())
|
||||||
|
data = found
|
||||||
|
}
|
||||||
|
if (found.media) {
|
||||||
|
image = found.media.large_url
|
||||||
|
} else if (found.banner) {
|
||||||
|
image = found.banner.large_url
|
||||||
|
}
|
||||||
|
if (found.description) {
|
||||||
|
description = striptags(found.description)
|
||||||
|
}
|
||||||
|
if (found.parent) {
|
||||||
|
title = found.name + ' - ' + found.parent.name + ' - NFP Moe'
|
||||||
|
} else {
|
||||||
|
title = found.name + ' - NFP Moe'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ctx.log.error(e)
|
||||||
|
data = null
|
||||||
|
links = null
|
||||||
|
}
|
||||||
|
ctx.body = bodyTemplate({
|
||||||
|
v: config.get('CIRCLECI_VERSION'),
|
||||||
|
tree: JSON.stringify(tree),
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
links: JSON.stringify(links),
|
||||||
|
image: image,
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
})
|
||||||
|
ctx.set('Content-Length', Buffer.byteLength(ctx.body))
|
||||||
|
ctx.set('Cache-Control', 'max-age=0')
|
||||||
|
ctx.set('Content-Type', 'text/html; charset=utf-8')
|
||||||
|
}
|
13
app/admin.js
13
app/admin.js
|
@ -5,11 +5,8 @@ const EditArticle = require('./admin/editarticle')
|
||||||
const AdminStaffList = require('./admin/stafflist')
|
const AdminStaffList = require('./admin/stafflist')
|
||||||
const EditStaff = require('./admin/editstaff')
|
const EditStaff = require('./admin/editstaff')
|
||||||
|
|
||||||
window.addAdminRoutes = [
|
window.adminRoutes = {
|
||||||
['/admin/pages', AdminPages],
|
pages: [AdminPages, EditPage],
|
||||||
['/admin/pages/:key', EditPage],
|
articles: [AdminArticles, EditArticle],
|
||||||
['/admin/articles', AdminArticles],
|
staff: [AdminStaffList, EditStaff],
|
||||||
['/admin/articles/:id', EditArticle],
|
}
|
||||||
['/admin/staff', AdminStaffList],
|
|
||||||
['/admin/staff/:id', EditStaff],
|
|
||||||
]
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ const AdminArticles = {
|
||||||
this.links = null
|
this.links = null
|
||||||
this.lastpage = m.route.param('page') || '1'
|
this.lastpage = m.route.param('page') || '1'
|
||||||
|
|
||||||
|
document.title = 'Articles Page ' + this.lastpage + ' - Admin NFP Moe'
|
||||||
|
|
||||||
return pagination.fetchPage(Article.getAllArticlesPagination({
|
return pagination.fetchPage(Article.getAllArticlesPagination({
|
||||||
per_page: 10,
|
per_page: 10,
|
||||||
page: this.lastpage,
|
page: this.lastpage,
|
||||||
|
|
|
@ -45,6 +45,9 @@ const EditArticle = {
|
||||||
onupdate: function(vnode) {
|
onupdate: function(vnode) {
|
||||||
if (this.lastid !== m.route.param('id')) {
|
if (this.lastid !== m.route.param('id')) {
|
||||||
this.fetchArticle(vnode)
|
this.fetchArticle(vnode)
|
||||||
|
if (this.lastid === 'add') {
|
||||||
|
m.redraw()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -63,7 +66,6 @@ const EditArticle = {
|
||||||
files: [],
|
files: [],
|
||||||
}
|
}
|
||||||
this.editedPath = false
|
this.editedPath = false
|
||||||
this.froala = null
|
|
||||||
this.loadedFroala = Froala.loadedFroala
|
this.loadedFroala = Froala.loadedFroala
|
||||||
|
|
||||||
if (this.lastid !== 'add') {
|
if (this.lastid !== 'add') {
|
||||||
|
@ -71,14 +73,23 @@ const EditArticle = {
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
vnode.state.editedPath = true
|
vnode.state.editedPath = true
|
||||||
vnode.state.article = result
|
vnode.state.article = result
|
||||||
|
document.title = 'Editing: ' + result.name + ' - Admin NFP Moe'
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
vnode.state.error = err.message
|
vnode.state.error = err.message
|
||||||
})
|
})
|
||||||
.then(function() {
|
.then(function() {
|
||||||
vnode.state.loading = false
|
vnode.state.loading = false
|
||||||
|
if (vnode.state.froala) {
|
||||||
|
vnode.state.froala.html.set(vnode.state.article.description)
|
||||||
|
}
|
||||||
m.redraw()
|
m.redraw()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
document.title = 'Create Article - Admin NFP Moe'
|
||||||
|
if (vnode.state.froala) {
|
||||||
|
vnode.state.froala.html.set(this.article.description)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -216,6 +227,7 @@ const EditArticle = {
|
||||||
onclick: function() { vnode.state.error = '' },
|
onclick: function() { vnode.state.error = '' },
|
||||||
}, this.error),
|
}, this.error),
|
||||||
m(FileUpload, {
|
m(FileUpload, {
|
||||||
|
height: 300,
|
||||||
onupload: this.mediaUploaded.bind(this, 'banner'),
|
onupload: this.mediaUploaded.bind(this, 'banner'),
|
||||||
onerror: function(e) { vnode.state.error = e },
|
onerror: function(e) { vnode.state.error = e },
|
||||||
ondelete: this.mediaRemoved.bind(this, 'banner'),
|
ondelete: this.mediaRemoved.bind(this, 'banner'),
|
||||||
|
|
|
@ -43,6 +43,7 @@ const EditPage = {
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
vnode.state.editedPath = true
|
vnode.state.editedPath = true
|
||||||
vnode.state.page = result
|
vnode.state.page = result
|
||||||
|
document.title = 'Editing: ' + result.name + ' - Admin NFP Moe'
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
vnode.state.error = err.message
|
vnode.state.error = err.message
|
||||||
|
@ -51,6 +52,8 @@ const EditPage = {
|
||||||
vnode.state.loading = false
|
vnode.state.loading = false
|
||||||
m.redraw()
|
m.redraw()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
document.title = 'Create Page - Admin NFP Moe'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.loadedFroala) {
|
if (!this.loadedFroala) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ const EditStaff = {
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
vnode.state.editedPath = true
|
vnode.state.editedPath = true
|
||||||
vnode.state.staff = result
|
vnode.state.staff = result
|
||||||
|
document.title = 'Editing: ' + result.fullname + ' - Admin NFP Moe'
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
vnode.state.error = err.message
|
vnode.state.error = err.message
|
||||||
|
@ -36,6 +37,8 @@ const EditStaff = {
|
||||||
vnode.state.loading = false
|
vnode.state.loading = false
|
||||||
m.redraw()
|
m.redraw()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
document.title = 'Creating Staff Member - Admin NFP Moe'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ const AdminPages = {
|
||||||
this.pages = []
|
this.pages = []
|
||||||
this.removePage = null
|
this.removePage = null
|
||||||
|
|
||||||
|
document.title = 'Pages - Admin NFP Moe'
|
||||||
|
|
||||||
Page.getAllPages()
|
Page.getAllPages()
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
vnode.state.pages = AdminPages.parseTree(result)
|
vnode.state.pages = AdminPages.parseTree(result)
|
||||||
|
|
|
@ -15,6 +15,8 @@ const AdminStaffList = {
|
||||||
fetchStaffs: function(vnode) {
|
fetchStaffs: function(vnode) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
|
document.title = 'Staff members - Admin NFP Moe'
|
||||||
|
|
||||||
return Staff.getAllStaff()
|
return Staff.getAllStaff()
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
vnode.state.staff = result
|
vnode.state.staff = result
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
const common = require('./common')
|
const common = require('./common')
|
||||||
|
|
||||||
exports.uploadMedia = function(file) {
|
exports.uploadMedia = function(file, height) {
|
||||||
let formData = new FormData()
|
let formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|
||||||
|
let extra = ''
|
||||||
|
if (height) {
|
||||||
|
extra = '?height=' + height
|
||||||
|
}
|
||||||
|
|
||||||
return common.sendRequest({
|
return common.sendRequest({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/media',
|
url: '/api/media' + extra,
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,6 @@ exports.getTree = function() {
|
||||||
exports.getPage = function(id) {
|
exports.getPage = function(id) {
|
||||||
return common.sendRequest({
|
return common.sendRequest({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/pages/' + id + '?includes=media,banner,children,news,news.media',
|
url: '/api/pages/' + id + '?includes=media,banner,children',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,18 @@ const Article = {
|
||||||
this.error = ''
|
this.error = ''
|
||||||
this.lastarticle = m.route.param('article') || '1'
|
this.lastarticle = m.route.param('article') || '1'
|
||||||
this.loadingnews = false
|
this.loadingnews = false
|
||||||
|
|
||||||
|
if (window.__nfpdata) {
|
||||||
|
this.path = m.route.param('id')
|
||||||
|
this.article = window.__nfpdata
|
||||||
|
window.__nfpdata = null
|
||||||
|
} else {
|
||||||
this.fetchArticle(vnode)
|
this.fetchArticle(vnode)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchArticle: function(vnode) {
|
fetchArticle: function(vnode) {
|
||||||
this.path = m.route.param('id')
|
this.path = m.route.param('id')
|
||||||
this.news = []
|
|
||||||
this.newslinks = null
|
|
||||||
this.article = {
|
this.article = {
|
||||||
id: 0,
|
id: 0,
|
||||||
name: '',
|
name: '',
|
||||||
|
@ -29,6 +34,11 @@ const Article = {
|
||||||
ApiArticle.getArticle(this.path)
|
ApiArticle.getArticle(this.path)
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
vnode.state.article = result
|
vnode.state.article = result
|
||||||
|
if (result.parent) {
|
||||||
|
document.title = result.name + ' - ' + result.parent.name + ' - NFP Moe'
|
||||||
|
} else {
|
||||||
|
document.title = result.name + ' - NFP Moe'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
vnode.state.error = err.message
|
vnode.state.error = err.message
|
||||||
|
|
|
@ -101,7 +101,15 @@ only screen and ( min-resolution: 2dppx) {
|
||||||
@media (pointer:coarse) {
|
@media (pointer:coarse) {
|
||||||
footer .sitemap a.root,
|
footer .sitemap a.root,
|
||||||
footer .sitemap a.child {
|
footer .sitemap a.child {
|
||||||
// padding: 10px 10px;
|
padding: 10px 10px;
|
||||||
// display: inline-block;
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px){
|
||||||
|
footer .sitemap a.root,
|
||||||
|
footer .sitemap a.child {
|
||||||
|
padding: 9px 10px;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,15 @@ const Frontpage = {
|
||||||
&& window.__nfplinks) {
|
&& window.__nfplinks) {
|
||||||
this.links = window.__nfplinks
|
this.links = window.__nfplinks
|
||||||
this.articles = window.__nfpdata
|
this.articles = window.__nfpdata
|
||||||
this.lastpage = '1'
|
this.lastpage = m.route.param('page') || '1'
|
||||||
window.__nfpdata = null
|
window.__nfpdata = null
|
||||||
window.__nfplinks = null
|
window.__nfplinks = null
|
||||||
|
|
||||||
|
if (this.articles.length === 0) {
|
||||||
|
m.route.set('/')
|
||||||
|
} else {
|
||||||
Frontpage.processFeatured(vnode, this.articles)
|
Frontpage.processFeatured(vnode, this.articles)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.fetchArticles(vnode)
|
this.fetchArticles(vnode)
|
||||||
}
|
}
|
||||||
|
@ -39,6 +44,12 @@ const Frontpage = {
|
||||||
this.articles = []
|
this.articles = []
|
||||||
this.lastpage = m.route.param('page') || '1'
|
this.lastpage = m.route.param('page') || '1'
|
||||||
|
|
||||||
|
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 Pagination.fetchPage(Article.getAllArticlesPagination({
|
return Pagination.fetchPage(Article.getAllArticlesPagination({
|
||||||
per_page: 10,
|
per_page: 10,
|
||||||
page: this.lastpage,
|
page: this.lastpage,
|
||||||
|
@ -59,6 +70,7 @@ const Frontpage = {
|
||||||
},
|
},
|
||||||
|
|
||||||
processFeatured: function(vnode, data) {
|
processFeatured: function(vnode, data) {
|
||||||
|
if (vnode.state.featured) return
|
||||||
for (var i = data.length - 1; i >= 0; i--) {
|
for (var i = data.length - 1; i >= 0; i--) {
|
||||||
if (data[i].banner) {
|
if (data[i].banner) {
|
||||||
vnode.state.featured = data[i]
|
vnode.state.featured = data[i]
|
||||||
|
|
|
@ -143,6 +143,16 @@ frontpage {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frontpage aside.sidebar a {
|
||||||
|
padding: 9px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (pointer:coarse) {
|
||||||
|
frontpage aside.sidebar a {
|
||||||
|
padding: 9px 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.darkmodeon {
|
.darkmodeon {
|
||||||
|
|
92
app/index.js
92
app/index.js
|
@ -1,51 +1,29 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
window.m = m
|
window.m = m
|
||||||
|
|
||||||
m.route.prefix = ''
|
|
||||||
|
|
||||||
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 Authentication = require('./authentication')
|
const Authentication = require('./authentication')
|
||||||
|
|
||||||
const menuRoot = document.getElementById('nav')
|
m.route.prefix = ''
|
||||||
const mainRoot = document.getElementById('main')
|
window.adminRoutes = {}
|
||||||
const footerRoot = document.getElementById('footer')
|
|
||||||
|
|
||||||
const allRoutes = {
|
|
||||||
'/': Frontpage,
|
|
||||||
'/login': Login,
|
|
||||||
'/logout': Logout,
|
|
||||||
'/page/:id': Page,
|
|
||||||
'/article/:id': Article,
|
|
||||||
}
|
|
||||||
|
|
||||||
m.route(mainRoot, '/', allRoutes)
|
|
||||||
m.mount(menuRoot, Menu)
|
|
||||||
m.mount(footerRoot, Footer)
|
|
||||||
|
|
||||||
let loadingAdmin = false
|
let loadingAdmin = false
|
||||||
let loadedAdmin = false
|
let loadedAdmin = false
|
||||||
let loaded = 0
|
let loaded = 0
|
||||||
|
let elements = []
|
||||||
|
|
||||||
const onLoaded = function() {
|
const onLoaded = function() {
|
||||||
loaded++
|
loaded++
|
||||||
if (loaded < 2) return
|
if (loaded < 2) return
|
||||||
|
|
||||||
if (window.addAdminRoutes) {
|
|
||||||
window.addAdminRoutes.forEach(function (item) {
|
|
||||||
allRoutes[item[0]] = item[1]
|
|
||||||
})
|
|
||||||
m.route(mainRoot, '/', allRoutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.level >= 10)
|
Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.level >= 10)
|
||||||
loadedAdmin = true
|
loadedAdmin = true
|
||||||
m.redraw()
|
m.route.set(m.route.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
const onError = function() {
|
||||||
|
elements.forEach(function(x) { x.remove() })
|
||||||
|
loadedAdmin = loadingAdmin = false
|
||||||
|
loaded = 0
|
||||||
|
m.route.set('/logout')
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadAdmin = function(user) {
|
const loadAdmin = function(user) {
|
||||||
|
@ -59,17 +37,22 @@ const loadAdmin = function(user) {
|
||||||
|
|
||||||
loadingAdmin = true
|
loadingAdmin = true
|
||||||
|
|
||||||
|
let token = Authentication.getToken()
|
||||||
let element = document.createElement('link')
|
let element = document.createElement('link')
|
||||||
|
elements.push(element)
|
||||||
element.setAttribute('rel', 'stylesheet')
|
element.setAttribute('rel', 'stylesheet')
|
||||||
element.setAttribute('type', 'text/css')
|
element.setAttribute('type', 'text/css')
|
||||||
element.setAttribute('href', '/assets/admin.css')
|
element.setAttribute('href', '/assets/admin.css?token=' + token)
|
||||||
element.onload = onLoaded
|
element.onload = onLoaded
|
||||||
|
element.onerror = onError
|
||||||
document.getElementsByTagName('head')[0].appendChild(element)
|
document.getElementsByTagName('head')[0].appendChild(element)
|
||||||
|
|
||||||
element = document.createElement('script')
|
element = document.createElement('script')
|
||||||
|
elements.push(element)
|
||||||
element.setAttribute('type', 'text/javascript')
|
element.setAttribute('type', 'text/javascript')
|
||||||
element.setAttribute('src', '/assets/admin.js')
|
element.setAttribute('src', '/assets/admin.js?token=' + token)
|
||||||
element.onload = onLoaded
|
element.onload = onLoaded
|
||||||
|
element.onerror = onError
|
||||||
document.body.appendChild(element)
|
document.body.appendChild(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,3 +60,42 @@ Authentication.addEvent(loadAdmin)
|
||||||
if (Authentication.currentUser) {
|
if (Authentication.currentUser) {
|
||||||
loadAdmin(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,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.route(mainRoot, '/', allRoutes)
|
||||||
|
m.mount(menuRoot, Menu)
|
||||||
|
m.mount(footerRoot, Footer)
|
||||||
|
|
|
@ -6,12 +6,12 @@ const Logout = {
|
||||||
Authentication.createGoogleScript()
|
Authentication.createGoogleScript()
|
||||||
.then(function() {
|
.then(function() {
|
||||||
return new Promise(function (res) {
|
return new Promise(function (res) {
|
||||||
gapi.load('auth2', res);
|
gapi.load('auth2', res)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(function() { return gapi.auth2.init() })
|
.then(function() { return gapi.auth2.init() })
|
||||||
.then(function() {
|
.then(function() {
|
||||||
let auth2 = gapi.auth2.getAuthInstance();
|
let auth2 = gapi.auth2.getAuthInstance()
|
||||||
return auth2.signOut()
|
return auth2.signOut()
|
||||||
})
|
})
|
||||||
.then(function() {
|
.then(function() {
|
||||||
|
|
|
@ -11,7 +11,17 @@ const Page = {
|
||||||
this.error = ''
|
this.error = ''
|
||||||
this.lastpage = m.route.param('page') || '1'
|
this.lastpage = m.route.param('page') || '1'
|
||||||
this.loadingnews = false
|
this.loadingnews = false
|
||||||
|
|
||||||
|
if (window.__nfpdata) {
|
||||||
|
this.path = m.route.param('id')
|
||||||
|
this.page = window.__nfpdata
|
||||||
|
this.news = []
|
||||||
|
this.newslinks = null
|
||||||
|
window.__nfpdata = null
|
||||||
|
vnode.state.fetchArticles(vnode)
|
||||||
|
} else {
|
||||||
this.fetchPage(vnode)
|
this.fetchPage(vnode)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchPage: function(vnode) {
|
fetchPage: function(vnode) {
|
||||||
|
@ -30,6 +40,7 @@ const Page = {
|
||||||
ApiPage.getPage(this.path)
|
ApiPage.getPage(this.path)
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
vnode.state.page = result
|
vnode.state.page = result
|
||||||
|
document.title = result.name + ' - NFP Moe'
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
vnode.state.error = err.message
|
vnode.state.error = err.message
|
||||||
|
@ -71,11 +82,26 @@ const Page = {
|
||||||
},
|
},
|
||||||
|
|
||||||
view: function(vnode) {
|
view: function(vnode) {
|
||||||
|
var deviceWidth = window.innerWidth
|
||||||
|
var bannerPath = ''
|
||||||
|
|
||||||
|
if (this.page && this.page.banner) {
|
||||||
|
var pixelRatio = window.devicePixelRatio || 1
|
||||||
|
if (deviceWidth < 400 && pixelRatio <= 1) {
|
||||||
|
bannerPath = this.page.banner.small_url
|
||||||
|
} else if ((deviceWidth < 800 && pixelRatio <= 1)
|
||||||
|
|| (deviceWidth < 600 && pixelRatio > 1)) {
|
||||||
|
bannerPath = this.page.banner.medium_url
|
||||||
|
} else {
|
||||||
|
bannerPath = this.page.banner.large_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.loading ?
|
this.loading ?
|
||||||
m('div.loading-spinner')
|
m('div.loading-spinner')
|
||||||
: m('article.page', [
|
: m('article.page', [
|
||||||
this.page.banner ? m('.div.page-banner', { style: { 'background-image': 'url("' + this.page.banner.url + '")' } } ) : null,
|
bannerPath ? m('.div.page-banner', { style: { 'background-image': 'url("' + bannerPath + '")' } } ) : null,
|
||||||
m('header', m('h1', this.page.name)),
|
m('header', m('h1', this.page.name)),
|
||||||
m('.container', {
|
m('.container', {
|
||||||
class: this.page.children.length ? 'multi' : '',
|
class: this.page.children.length ? 'multi' : '',
|
||||||
|
@ -90,7 +116,7 @@ const Page = {
|
||||||
: null,
|
: null,
|
||||||
this.page.description
|
this.page.description
|
||||||
? m('.fr-view', [
|
? m('.fr-view', [
|
||||||
this.page.media ? m('img.page-cover', { src: this.page.media.url, alt: 'Cover image for ' + this.page.name } ) : null,
|
this.page.media ? m('img.page-cover', { src: this.page.media.medium_url, alt: 'Cover image for ' + this.page.name } ) : null,
|
||||||
m.trust(this.page.description),
|
m.trust(this.page.description),
|
||||||
this.news.length && this.page.description
|
this.news.length && this.page.description
|
||||||
? m('aside.news', [
|
? m('aside.news', [
|
||||||
|
@ -107,7 +133,7 @@ const Page = {
|
||||||
])
|
])
|
||||||
: this.news.length
|
: this.news.length
|
||||||
? m('aside.news.single', [
|
? m('aside.news.single', [
|
||||||
this.page.media ? m('img.page-cover', { src: this.page.media.url, alt: 'Cover image for ' + this.page.name } ) : null,
|
this.page.media ? m('img.page-cover', { src: this.page.media.medium_url, alt: 'Cover image for ' + this.page.name } ) : null,
|
||||||
m('h4', 'Latest posts under ' + this.page.name + ':'),
|
m('h4', 'Latest posts under ' + this.page.name + ':'),
|
||||||
this.loadingnews ? m('div.loading-spinner') : this.news.map(function(article) {
|
this.loadingnews ? m('div.loading-spinner') : this.news.map(function(article) {
|
||||||
return m(Newsentry, article)
|
return m(Newsentry, article)
|
||||||
|
@ -118,7 +144,7 @@ const Page = {
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
: this.page.media
|
: this.page.media
|
||||||
? m('img.page-cover.single', { src: this.page.media.url, alt: 'Cover image for ' + this.page.name } )
|
? m('img.page-cover.single', { src: this.page.media.medium_url, alt: 'Cover image for ' + this.page.name } )
|
||||||
: null,
|
: null,
|
||||||
]),
|
]),
|
||||||
Authentication.currentUser
|
Authentication.currentUser
|
||||||
|
|
|
@ -6,7 +6,7 @@ const FileUpload = {
|
||||||
vnode.state.updateError(vnode, '')
|
vnode.state.updateError(vnode, '')
|
||||||
vnode.state.loading = true
|
vnode.state.loading = true
|
||||||
|
|
||||||
Media.uploadMedia(event.target.files[0])
|
Media.uploadMedia(event.target.files[0], vnode.attrs.height || null)
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
if (vnode.attrs.onupload) {
|
if (vnode.attrs.onupload) {
|
||||||
vnode.attrs.onupload(res)
|
vnode.attrs.onupload(res)
|
||||||
|
|
|
@ -66,7 +66,8 @@
|
||||||
"nconf": "^0.10.0",
|
"nconf": "^0.10.0",
|
||||||
"parse-torrent": "^7.0.1",
|
"parse-torrent": "^7.0.1",
|
||||||
"pg": "^7.8.0",
|
"pg": "^7.8.0",
|
||||||
"sharp": "^0.22.1"
|
"sharp": "^0.22.1",
|
||||||
|
"striptags": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browserify": "^16.2.3",
|
"browserify": "^16.2.3",
|
||||||
|
|
BIN
public/assets/img/heart.jpg
Normal file
BIN
public/assets/img/heart.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
public/assets/img/heart.xcf
Normal file
BIN
public/assets/img/heart.xcf
Normal file
Binary file not shown.
|
@ -2,9 +2,20 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>NFP Moe</title>
|
<title>{{=it.title}}</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
|
<meta name="description" content="{{=it.description}}">
|
||||||
|
<meta name="twitter:card" value="summary">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta id="ogimage" property="og:image" content="{{=it.image}}" />
|
||||||
|
<meta property="og:description" content="{{=it.description}}" />
|
||||||
|
{{? it.image === '/assets/img/heart.jpg' }}
|
||||||
|
<meta id="ogimagewidth" property="og:image:width" content="400" />
|
||||||
|
<meta id="ogimageheight" property="og:image:height" content="500" />
|
||||||
|
{{?? true }}
|
||||||
|
{{? }}
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
|
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
|
||||||
<link rel="Stylesheet" href="/assets/app.css?v={{=it.v}}" type="text/css" />
|
<link rel="Stylesheet" href="/assets/app.css?v={{=it.v}}" type="text/css" />
|
||||||
<link rel="preconnect" href="https://cdn-nfp.global.ssl.fastly.net" />
|
<link rel="preconnect" href="https://cdn-nfp.global.ssl.fastly.net" />
|
||||||
|
|
Loading…
Reference in a new issue