filadelfia_web: init
This commit is contained in:
parent
5baf1823f4
commit
825cd7ef2d
20 changed files with 896 additions and 0 deletions
24
filadelfia_web/api/serve.mjs
Normal file
24
filadelfia_web/api/serve.mjs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import dot from 'dot'
|
||||||
|
import Parent from '../base/serve.mjs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default class ServeHandler extends Parent {
|
||||||
|
loadTemplate(indexFile) {
|
||||||
|
this.template = dot.template(indexFile.toString(), { argName: [
|
||||||
|
'siteUrl',
|
||||||
|
] })
|
||||||
|
}
|
||||||
|
|
||||||
|
async serveIndex(ctx) {
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
let indexFile = await fs.readFile(path.join(this.root, 'index.html'))
|
||||||
|
this.loadTemplate(indexFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = this.template({
|
||||||
|
siteUrl: this.frontend + ctx.url,
|
||||||
|
})
|
||||||
|
ctx.type = 'text/html; charset=utf-8'
|
||||||
|
}
|
||||||
|
}
|
26
filadelfia_web/api/server.mjs
Normal file
26
filadelfia_web/api/server.mjs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import config from '../base/config.mjs'
|
||||||
|
import Parent from '../base/server.mjs'
|
||||||
|
import StaticRoutes from '../base/static_routes.mjs'
|
||||||
|
import ServeHandler from './serve.mjs'
|
||||||
|
import AuthenticationRoutes from '../base/authentication/routes.mjs'
|
||||||
|
import VideoRoutes from './video/routes.mjs'
|
||||||
|
|
||||||
|
export default class Server extends Parent {
|
||||||
|
init() {
|
||||||
|
super.init()
|
||||||
|
let localUtil = new this.core.sc.Util(import.meta.url)
|
||||||
|
|
||||||
|
delete this.flaskaOptions.appendHeaders['Content-Security-Policy']
|
||||||
|
this.flaskaOptions.nonce = []
|
||||||
|
this.routes = {
|
||||||
|
static: new StaticRoutes(),
|
||||||
|
auth: new AuthenticationRoutes(),
|
||||||
|
videos: new VideoRoutes(),
|
||||||
|
}
|
||||||
|
this.routes.serve = new ServeHandler({
|
||||||
|
root: localUtil.getPathFromRoot('../public'),
|
||||||
|
version: this.core.app.running,
|
||||||
|
frontend: config.get('frontend:url'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
36
filadelfia_web/api/video/routes.mjs
Normal file
36
filadelfia_web/api/video/routes.mjs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import config from '../../base/config.mjs'
|
||||||
|
import Client from '../../base/media/client.mjs'
|
||||||
|
import { deleteFile, uploadFile } from '../../base/media/upload.mjs'
|
||||||
|
import { parseVideos, parseVideo } from './util.mjs'
|
||||||
|
|
||||||
|
export default class VideoRoutes {
|
||||||
|
constructor(opts = {}) {
|
||||||
|
Object.assign(this, {
|
||||||
|
uploadFile: uploadFile,
|
||||||
|
deleteFile: deleteFile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
register(server) {
|
||||||
|
server.flaska.get('/api/videos', server.authenticate(), this.getVideos.bind(this))
|
||||||
|
server.flaska.get('/api/videos/uploadToken', server.authenticate(), this.getUploadToken.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET: /api/videos */
|
||||||
|
async getVideos(ctx) {
|
||||||
|
console.log(ctx.state.auth_token)
|
||||||
|
let res = await ctx.db.safeCallProc('filadelfia_archive.videos_auth_get_all', [ctx.state.auth_token])
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
videos: parseVideos(res.results[0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUploadToken(ctx) {
|
||||||
|
const media = config.get('media')
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: Client.createJwt({ iss: media.iss }, media.secret),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
filadelfia_web/api/video/util.mjs
Normal file
16
filadelfia_web/api/video/util.mjs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { parseMediaAndBanner } from "../../base/util.mjs"
|
||||||
|
|
||||||
|
export function parseVideos(videos) {
|
||||||
|
for (let i = 0; i < videos.length; i++) {
|
||||||
|
parseVideo(videos[i])
|
||||||
|
}
|
||||||
|
return videos
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseVideo(video) {
|
||||||
|
if (!video) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
parseMediaAndBanner(video)
|
||||||
|
return video
|
||||||
|
}
|
53
filadelfia_web/app/api.js
Normal file
53
filadelfia_web/app/api.js
Normal 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('/', { redirect: m.route.get() })
|
||||||
|
}
|
||||||
|
if (error.response && error.response.status) {
|
||||||
|
return Promise.reject(error.response)
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
|
}
|
42
filadelfia_web/app/authentication.js
Normal file
42
filadelfia_web/app/authentication.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
const storageName = 'nfp_sites_filadelfia_web_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
|
41
filadelfia_web/app/header.js
Normal file
41
filadelfia_web/app/header.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const videos = require('./videos')
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
|
||||||
|
const Menu = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.currentActive = 'home'
|
||||||
|
this.loading = false
|
||||||
|
this.onbeforeupdate()
|
||||||
|
},
|
||||||
|
|
||||||
|
onbeforeupdate: function() {
|
||||||
|
let currentPath = m.route.get()
|
||||||
|
|
||||||
|
/*if (currentPath === '/') this.currentActive = 'home'
|
||||||
|
else if (currentPath === '/login') this.currentActive = 'login'
|
||||||
|
else if (currentPath && currentPath.startsWith('/page')) this.currentActive = currentPath.slice(currentPath.lastIndexOf('/') + 1)*/
|
||||||
|
},
|
||||||
|
|
||||||
|
logOut: function() {
|
||||||
|
Authentication.clearToken()
|
||||||
|
m.route.set('/')
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function() {
|
||||||
|
console.log(Authentication.currentUser)
|
||||||
|
return Authentication.currentUser
|
||||||
|
? [
|
||||||
|
m('nav', [
|
||||||
|
m('h4', m(m.route.Link, { href: '/browse' }, 'Filadelfia archival center')),
|
||||||
|
Authentication.currentUser.rank > 10
|
||||||
|
? m(m.route.Link, { class: 'upload', href: '/upload' }, 'Upload')
|
||||||
|
: null,
|
||||||
|
m('button.logout', { onclick: this.logOut }, 'Log out'),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
: null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Menu
|
46
filadelfia_web/app/index.js
Normal file
46
filadelfia_web/app/index.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
const Header = require('./header')
|
||||||
|
const Login = require('./page_login')
|
||||||
|
const Browse = require('./page_browse')
|
||||||
|
window.m = m
|
||||||
|
|
||||||
|
var fileref = document.createElement("link");
|
||||||
|
fileref.setAttribute("rel", "stylesheet");
|
||||||
|
fileref.setAttribute("type", "text/css");
|
||||||
|
fileref.setAttribute("href", '/assets/app.css?v=2');
|
||||||
|
document.head.appendChild(fileref)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.route.prefix = ''
|
||||||
|
|
||||||
|
const allRoutes = {
|
||||||
|
'/': Login,
|
||||||
|
'/browse': Browse,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.mount(document.getElementById('header'), Header)
|
||||||
|
m.route(document.getElementById('main'), '/', allRoutes)
|
||||||
|
}
|
||||||
|
AVIF.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAABcAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAABAA0ABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKCBgAPkgIaDQgMgkf8AAAQAAAr3A=';
|
31
filadelfia_web/app/page_browse.js
Normal file
31
filadelfia_web/app/page_browse.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
const videos = require('./videos')
|
||||||
|
|
||||||
|
const Browse = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
if (!videos.Tree.length) {
|
||||||
|
this.refreshTree()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate: function() {
|
||||||
|
if (Authentication.currentUser) return
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshTree: function(vnode) {
|
||||||
|
videos.refreshTree()
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return [
|
||||||
|
videos.error
|
||||||
|
? m('div.full-error', { onclick: this.refreshTree.bind(this) }, [
|
||||||
|
videos.error, m('br'), 'Click here to try again'
|
||||||
|
])
|
||||||
|
: null,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Browse
|
91
filadelfia_web/app/page_login.js
Normal file
91
filadelfia_web/app/page_login.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
const api = require('./api')
|
||||||
|
|
||||||
|
const Login = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.redirect = vnode.attrs.redirect || ''
|
||||||
|
if (Authentication.currentUser) return m.route.set('/browse')
|
||||||
|
|
||||||
|
this.error = ''
|
||||||
|
this.loading = false
|
||||||
|
this.username = ''
|
||||||
|
this.password = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
oncreate: function() {
|
||||||
|
if (Authentication.currentUser) return
|
||||||
|
},
|
||||||
|
|
||||||
|
loginuser: function(vnode, e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
if (!this.password) this.error = 'Password is missing'
|
||||||
|
if (!this.username) this.error = 'Email is missing'
|
||||||
|
|
||||||
|
if (this.error) return false
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
api.sendRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/authentication/login',
|
||||||
|
body: {
|
||||||
|
email: this.username,
|
||||||
|
password: this.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (!result.token) {
|
||||||
|
return Promise.reject(new Error('Server authentication down.'))
|
||||||
|
}
|
||||||
|
Authentication.updateToken(result.token)
|
||||||
|
m.route.set(this.redirect || '/browse')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.error = 'Error while logging in! ' + error.message
|
||||||
|
vnode.state.password = ''
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return [
|
||||||
|
m('div.modal', [
|
||||||
|
m('form', {
|
||||||
|
onsubmit: this.loginuser.bind(this, vnode),
|
||||||
|
}, [
|
||||||
|
m('h3', 'Filadelfia archival center'),
|
||||||
|
this.error ? m('p.error', this.error) : null,
|
||||||
|
m('label', 'Email or name'),
|
||||||
|
m('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: this.username,
|
||||||
|
oninput: (e) => { this.username = e.currentTarget.value },
|
||||||
|
}),
|
||||||
|
m('label', 'Password'),
|
||||||
|
m('input', {
|
||||||
|
type: 'password',
|
||||||
|
value: this.password,
|
||||||
|
oninput: (e) => { this.password = e.currentTarget.value },
|
||||||
|
}),
|
||||||
|
m('input.spinner', {
|
||||||
|
hidden: this.loading,
|
||||||
|
type: 'submit',
|
||||||
|
value: 'Log in',
|
||||||
|
}),
|
||||||
|
this.loading ? m('div.loading-spinner') : null,
|
||||||
|
]),
|
||||||
|
m('div.login--asuna.spritesheet'),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Login
|
91
filadelfia_web/app/page_upload.js
Normal file
91
filadelfia_web/app/page_upload.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const Authentication = require('./authentication')
|
||||||
|
const api = require('./api')
|
||||||
|
|
||||||
|
const Login = {
|
||||||
|
oninit: function(vnode) {
|
||||||
|
this.redirect = vnode.attrs.redirect || ''
|
||||||
|
if (!Authentication.currentUser) return m.route.set('/')
|
||||||
|
|
||||||
|
this.error = ''
|
||||||
|
this.loading = false
|
||||||
|
this.body = {
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
month: new Date().getMonth() + 1,
|
||||||
|
title: '',
|
||||||
|
file: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadfile: function(vnode, e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
if (!this.password) this.error = 'Password is missing'
|
||||||
|
if (!this.username) this.error = 'Email is missing'
|
||||||
|
|
||||||
|
if (this.error) return false
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
api.sendRequest({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/authentication/login',
|
||||||
|
body: {
|
||||||
|
email: this.username,
|
||||||
|
password: this.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (!result.token) {
|
||||||
|
return Promise.reject(new Error('Server authentication down.'))
|
||||||
|
}
|
||||||
|
Authentication.updateToken(result.token)
|
||||||
|
m.route.set(this.redirect || '/browse')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.error = 'Error while logging in! ' + error.message
|
||||||
|
vnode.state.password = ''
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function(vnode) {
|
||||||
|
return [
|
||||||
|
m('div.modal', [
|
||||||
|
m('form', {
|
||||||
|
onsubmit: this.uploadfile.bind(this, vnode),
|
||||||
|
}, [
|
||||||
|
m('h3', 'Filadelfia archival center'),
|
||||||
|
this.error ? m('p.error', this.error) : null,
|
||||||
|
m('label', 'Email or name'),
|
||||||
|
m('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: this.username,
|
||||||
|
oninput: (e) => { this.username = e.currentTarget.value },
|
||||||
|
}),
|
||||||
|
m('label', 'Password'),
|
||||||
|
m('input', {
|
||||||
|
type: 'password',
|
||||||
|
value: this.password,
|
||||||
|
oninput: (e) => { this.password = e.currentTarget.value },
|
||||||
|
}),
|
||||||
|
m('input.spinner', {
|
||||||
|
hidden: this.loading,
|
||||||
|
type: 'submit',
|
||||||
|
value: 'Log in',
|
||||||
|
}),
|
||||||
|
this.loading ? m('div.loading-spinner') : null,
|
||||||
|
]),
|
||||||
|
m('div.login--asuna.spritesheet'),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Login
|
35
filadelfia_web/app/videos.js
Normal file
35
filadelfia_web/app/videos.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const api = require('./api')
|
||||||
|
|
||||||
|
const Tree = []
|
||||||
|
|
||||||
|
exports.Tree = Tree
|
||||||
|
|
||||||
|
exports.loading = false
|
||||||
|
exports.error = ''
|
||||||
|
|
||||||
|
exports.refreshTree = function() {
|
||||||
|
exports.error = ''
|
||||||
|
|
||||||
|
if (exports.loading) return Promise.resolve()
|
||||||
|
|
||||||
|
exports.loading = true
|
||||||
|
|
||||||
|
m.redraw()
|
||||||
|
|
||||||
|
return api.sendRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/videos',
|
||||||
|
})
|
||||||
|
.then(pages => {
|
||||||
|
console.log(pages)
|
||||||
|
Tree.splice(0, Tree.length)
|
||||||
|
Tree.push.apply(Tree, pages.videos)
|
||||||
|
exports.loading = false
|
||||||
|
m.redraw()
|
||||||
|
}, err => {
|
||||||
|
exports.loading = false
|
||||||
|
m.redraw()
|
||||||
|
exports.error = 'Error fetching videos: ' + err.message
|
||||||
|
})
|
||||||
|
}
|
1
filadelfia_web/base
Symbolic link
1
filadelfia_web/base
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../base
|
8
filadelfia_web/build-package.json
Normal file
8
filadelfia_web/build-package.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build": "echo done;"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"service-core": "^3.0.0-beta.17"
|
||||||
|
}
|
||||||
|
}
|
24
filadelfia_web/dev.mjs
Normal file
24
filadelfia_web/dev.mjs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import { ServiceCore } from 'service-core'
|
||||||
|
import * as index from './index.mjs'
|
||||||
|
|
||||||
|
const port = 4130
|
||||||
|
|
||||||
|
var core = new ServiceCore('filadelfia_web', import.meta.url, port, '')
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
frontend: {
|
||||||
|
url: 'http://localhost:' + port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = JSON.parse(fs.readFileSync('./config.json'))
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
config.port = port
|
||||||
|
|
||||||
|
core.setConfig(config)
|
||||||
|
core.init(index).then(function() {
|
||||||
|
return core.run()
|
||||||
|
})
|
11
filadelfia_web/index.mjs
Normal file
11
filadelfia_web/index.mjs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import config from './base/config.mjs'
|
||||||
|
|
||||||
|
export function start(http, port, ctx) {
|
||||||
|
config.sources[1].store = ctx.config
|
||||||
|
|
||||||
|
return import('./api/server.mjs')
|
||||||
|
.then(function(module) {
|
||||||
|
let server = new module.default(http, port, ctx)
|
||||||
|
return server.run()
|
||||||
|
})
|
||||||
|
}
|
61
filadelfia_web/package.json
Normal file
61
filadelfia_web/package.json
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"name": "filadelfia_web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"port": 4130,
|
||||||
|
"description": "Filadelfia web portal",
|
||||||
|
"main": "index.js",
|
||||||
|
"directories": {
|
||||||
|
"test": "test"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.mjs",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build:prod": "asbundle app/index.js public/assets/app.js",
|
||||||
|
"build": "asbundle app/index.js public/assets/app.js",
|
||||||
|
"dev:build": "eltro --watch build --npm build",
|
||||||
|
"dev:server": "eltro --watch server --npm server",
|
||||||
|
"server": "node dev.mjs | bunyan"
|
||||||
|
},
|
||||||
|
"watch": {
|
||||||
|
"server": {
|
||||||
|
"patterns": [
|
||||||
|
"api",
|
||||||
|
"base",
|
||||||
|
"../base"
|
||||||
|
],
|
||||||
|
"extensions": "js,mjs",
|
||||||
|
"quiet": true,
|
||||||
|
"inherit": true
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"patterns": [
|
||||||
|
"app"
|
||||||
|
],
|
||||||
|
"extensions": "js",
|
||||||
|
"quiet": true,
|
||||||
|
"inherit": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.nfp.is/nfp/nfp_sites.git"
|
||||||
|
},
|
||||||
|
"author": "Jonatan Nilsson",
|
||||||
|
"license": "WTFPL",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://git.nfp.is/nfp/nfp_sites/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://git.nfp.is/nfp/nfp_sites",
|
||||||
|
"dependencies": {
|
||||||
|
"dot": "^2.0.0-beta.1",
|
||||||
|
"msnodesqlv8": "^2.4.7",
|
||||||
|
"nconf-lite": "^2.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"asbundle": "^2.6.1",
|
||||||
|
"eltro": "^1.4.4",
|
||||||
|
"flaska": "^1.3.2",
|
||||||
|
"mithril": "^2.2.2",
|
||||||
|
"service-core": "^3.0.0-beta.17"
|
||||||
|
}
|
||||||
|
}
|
13
filadelfia_web/public/assets/app.css
Normal file
13
filadelfia_web/public/assets/app.css
Normal file
File diff suppressed because one or more lines are too long
BIN
filadelfia_web/public/assets/bg.avif
Normal file
BIN
filadelfia_web/public/assets/bg.avif
Normal file
Binary file not shown.
246
filadelfia_web/public/index.html
Normal file
246
filadelfia_web/public/index.html
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Filadelfia web portal</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
|
<style>
|
||||||
|
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #fff;
|
||||||
|
--bg-component: #f3f7ff;
|
||||||
|
--bg-component-alt: #ffd99c;
|
||||||
|
--color: #031131;
|
||||||
|
--main: #1066ff;
|
||||||
|
--main-fg: #fff;
|
||||||
|
--error: red;
|
||||||
|
--error-bg: hsl(0, 75%, 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Box sizing rules */
|
||||||
|
*, *::before, *::after { box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default margin */
|
||||||
|
body, h1, h2, h3, h4, p, figure, blockquote, dl, dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
text-rendering: optimizeSpeed;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: 'Inter var', Helvetica, Arial, sans-serif;
|
||||||
|
font-variation-settings: "slnt" 0;
|
||||||
|
font-feature-settings: "case", "frac", "tnum", "ss02", "calt", "ccmp", "kern";
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.italic { font-variation-settings: "slnt" 10deg; }
|
||||||
|
|
||||||
|
input, button, textarea, select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.88rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.66rem;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.44rem;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 1.22rem;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size: 1.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:visited, button {
|
||||||
|
text-decoration: underline;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--main);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
input[type=text],
|
||||||
|
input[type=password] {
|
||||||
|
border: 1px solid var(--main);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--color);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0.25rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button, input[type=submit] {
|
||||||
|
background: var(--main);
|
||||||
|
color:var(--main-fg);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
border: none;
|
||||||
|
margin: 1rem 0 2rem;
|
||||||
|
align-self: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.spinner, input[type=submit].spinner {
|
||||||
|
height: 2rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text]:focus {
|
||||||
|
outline: 1px solid var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
flex: 2 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
flex: 2 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal h3 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form p, label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal form {
|
||||||
|
background: var(--bg-component);
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.loading-spinner:after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 6px solid var(--main);
|
||||||
|
border-color: var(--main) transparent var(--main) transparent;
|
||||||
|
animation: loading-spinner 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes loading-spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav */
|
||||||
|
|
||||||
|
#header nav {
|
||||||
|
display: flex;
|
||||||
|
background: var(--bg-component);
|
||||||
|
padding: 0.5rem 1rem 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header nav a,
|
||||||
|
#header nav button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header h4 {
|
||||||
|
flex: 2 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header h4 a,
|
||||||
|
#header h4 a:visited {
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#header .logout,
|
||||||
|
#header .upload {
|
||||||
|
padding: 0.25rem 1.5rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header .logout {
|
||||||
|
background: var(--bg-component-alt);
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#header .upload {
|
||||||
|
background: var(--main);
|
||||||
|
color: var(--main-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main */
|
||||||
|
|
||||||
|
.full-error {
|
||||||
|
background: var(--error-bg);
|
||||||
|
color: var(--color);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 2 1 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="header"></div>
|
||||||
|
<main id="main"></main>
|
||||||
|
<script type="text/javascript" src="/assets/app.js?v=2"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue