diff --git a/filadelfia_web/api/article/routes.mjs b/filadelfia_web/api/article/routes.mjs index 44b58e7..c14ddb6 100644 --- a/filadelfia_web/api/article/routes.mjs +++ b/filadelfia_web/api/article/routes.mjs @@ -3,6 +3,7 @@ import Client from '../../base/media/client.mjs' import OriginalArticleRoutes from '../../base/article/routes.mjs' import { deleteFile, uploadFile } from '../../base/media/upload.mjs' import { parseVideos, parseVideo } from './util.mjs' +import { RankLevels } from '../../base/authentication/security.mjs' export default class ArticleRoutes extends OriginalArticleRoutes { constructor(opts = {}) { @@ -16,16 +17,16 @@ export default class ArticleRoutes extends OriginalArticleRoutes { } register(server) { - server.flaska.get('/api/auth/articles', server.authenticate(), this.getVideos.bind(this)) + server.flaska.get('/api/articles', this.getVideos.bind(this)) + server.flaska.get('/api/articles/:path', this.getArticle.bind(this)) server.flaska.put('/api/auth/articles/:id', [server.authenticate(), server.jsonHandler()], this.updateCreateArticle.bind(this)) - server.flaska.get('/api/auth/articles/:id', server.authenticate(), this.auth_getSingleArticle.bind(this)) - server.flaska.get('/api/auth/uploadToken', server.authenticate(), this.getUploadToken.bind(this)) - server.flaska.delete('/api/auth/articles/:id', server.authenticate(), this.auth_removeSingleArticle.bind(this)) + server.flaska.get('/api/auth/uploadToken', server.authenticate(RankLevels.Admin), this.getUploadToken.bind(this)) + server.flaska.delete('/api/auth/articles/:id', server.authenticate(RankLevels.Admin), this.auth_removeSingleArticle.bind(this)) } /** GET: /api/auth/articles */ async getVideos(ctx) { - let res = await ctx.db.safeCallProc('filadelfia_archive.article_auth_get_all', [ctx.state.auth_token]) + let res = await ctx.db.safeCallProc('filadelfia_archive.article_get_all', []) ctx.body = { videos: parseVideos(res.results[0]), @@ -40,6 +41,7 @@ export default class ArticleRoutes extends OriginalArticleRoutes { token: Client.createJwt({ iss: media.iss }, media.secret), path: config.get('media:directFilePath'), resize: config.get('media:path'), + delete: config.get('media:removePath'), } } diff --git a/filadelfia_web/app/api.js b/filadelfia_web/app/api.js index 1dfc70b..4360d63 100644 --- a/filadelfia_web/app/api.js +++ b/filadelfia_web/app/api.js @@ -82,6 +82,64 @@ const api = { }) }, + uploadBanner: function(bannerFile, token) { + var data = new FormData() + data.append('file', bannerFile) + data.append('preview', JSON.stringify({ + "out": "base64", + "format": "avif", + "resize": { + "width": 360, + "height": 203, + "fit": "cover", + "withoutEnlargement": true, + "kernel": "mitchell" + }, + "avif": { + "quality": 50, + "effort": 9 + } + })) + data.append('medium', JSON.stringify({ + "format": "avif", + "resize": { + "width": 1280, + "height": 720, + "fit": "cover", + "withoutEnlargement": true, + "kernel": "mitchell" + }, + "avif": { + "quality": 75, + "effort": 3 + } + })) + + return api.sendRequest({ + method: 'POST', + url: token.resize + '?token=' + token.token, + body: data, + }) + .then(banner => { + api.sendRequest({ + method: 'DELETE', + url: token.delete + banner.filename + '?token=' + token.token, + }).catch(err => console.error(err)) + + return { + file: banner, + size: bannerFile.size, + medium: { + filename: banner.medium.filename, + path: banner.medium.path, + }, + preview: { + base64: banner.preview.base64, + }, + } + }) + }, + prettyFormatBytes: function(bytes) { if (bytes < 1024) { return `${bytes} B` diff --git a/filadelfia_web/app/authentication.js b/filadelfia_web/app/authentication.js index 768e43b..091f5fb 100644 --- a/filadelfia_web/app/authentication.js +++ b/filadelfia_web/app/authentication.js @@ -48,12 +48,12 @@ const Authentication = { requiresLogin: function() { if (Authentication.currentUser) return - m.route.set('/') + m.route.set('/login') }, requiresNotLogin: function() { if (!Authentication.currentUser) return - m.route.set('/browse') + m.route.set('/') }, } diff --git a/filadelfia_web/app/header.js b/filadelfia_web/app/header.js index 3dc217e..886de58 100644 --- a/filadelfia_web/app/header.js +++ b/filadelfia_web/app/header.js @@ -7,7 +7,6 @@ const Menu = { oninit: function(vnode) { this.currentActive = 'home' this.loading = false - this.browsing = true if (!videos.Tree.length) { videos.refreshTree() } @@ -17,7 +16,6 @@ const Menu = { onbeforeupdate: function() { videos.calculateActiveBranches() let currentPath = m.route.get() - this.browsing = currentPath === '/' || currentPath === '/browse' || videos.year }, logOut: function() { @@ -30,43 +28,44 @@ const Menu = { let last = videos.Tree[videos.Tree.length - 1] let hasId = m.route.param('id') - return Authentication.currentUser - ? [ - m('nav', [ - m('h4', m(m.route.Link, { href: '/browse' }, lang.header_title /* Filadelfia archival center */)), - m('a.change', { onclick: lang.langtoggle }, lang.lang_current), - Authentication.currentUser.rank > 10 - ? m(m.route.Link, { class: 'upload', href: '/upload' }, lang.upload_goto) // Upload - : null, - // m('button.logout', { onclick: this.logOut }, lang.header_logout), // Log out - ]), - !this.browsing && videos.error - ? m('div.error', { onclick: videos.refreshTree }, [ - videos.error, m('br'), 'Click here to try again' - ]) - : null, - this.browsing && !videos.error - ? [ - m('.nav', m('.inner', tree.map(year => { + return [ + m('nav', [ + m('h4', m(m.route.Link, { href: '/' }, lang.header_title /* Filadelfia archival center */)), + m('a.link.changelang', { onclick: lang.langtoggle }, lang.lang_current), + Authentication.currentUser?.rank >= 100 + ? m(m.route.Link, { class: 'upload', href: '/upload' }, lang.upload_goto) // Upload + : null, + Authentication.currentUser + ? m(m.route.Link, { class: 'link', href: '/logout' }, lang.logout) + : m(m.route.Link, { class: 'upload', href: '/login' }, lang.login_submit) // Upload, + // m('button.logout', { onclick: this.logOut }, lang.header_logout), // Log out + ]), + videos.error + ? m('div.error', { onclick: videos.refreshTree }, [ + videos.error, m('br'), 'Click here to try again' + ]) + : null, + !videos.error + ? [ + m('.nav', m('.inner', tree.map(year => { + return m(m.route.Link, { + class: [videos.year === year ? 'active' : '', + !year.videos.length ? 'empty' : ''].join(' '), + href: ['', (videos.year !== year && year !== last) || hasId ? year.title : '' ].filter(x => x).join('/') || '/', + }, year.title) + }))), + videos.year + ? m('.nav', m('.inner', videos.year.branches.map(month => { return m(m.route.Link, { - class: [videos.year === year ? 'active' : '', - !year.videos.length ? 'empty' : ''].join(' '), - href: ['', (videos.year !== year && year !== last) || hasId ? year.title : 'browse' ].filter(x => x).join('/'), - }, year.title) - }))), - videos.year - ? m('.nav', m('.inner', videos.year.branches.map(month => { - return m(m.route.Link, { - class: [videos.month === month ? 'active' : '', - !month.videos.length ? 'empty' : ''].join(' '), - href: ['', videos.year.title, videos.month !== month || hasId ? month.title : null ].filter(x => x).join('/'), - }, lang.months[month.title]) - }))) - : null, - ] - : null, - ] - : null + class: [videos.month === month ? 'active' : '', + !month.videos.length ? 'empty' : ''].join(' '), + href: ['', videos.year.title, videos.month !== month || hasId ? month.title : null ].filter(x => x).join('/'), + }, lang.months[month.title]) + }))) + : null, + ] + : null, + ] }, } diff --git a/filadelfia_web/app/holdbutton.js b/filadelfia_web/app/holdbutton.js new file mode 100644 index 0000000..25f27d8 --- /dev/null +++ b/filadelfia_web/app/holdbutton.js @@ -0,0 +1,68 @@ +const m = require('mithril') + +const HoldButton = { + oninit: function(vnode) { + this.timer = null + this.holding = false + this.windowBlur = () => { + this.timerStop() + m.redraw() + } + window.addEventListener('blur', this.windowBlur) + }, + + onremove: function(vnode) { + this.timerStop() + window.removeEventListener('blur', this.windowBlur) + }, + + keydown(vnode, e) { + if (e.key === " " || e.key === "Enter" || e.key === "Spacebar") { + this.timerStart(vnode) + m.redraw() + } + }, + + keyup(e) { + if (e.key === " " || e.key === "Enter" || e.key === "Spacebar") { + this.timerStop() + m.redraw() + } + }, + + timerStart(vnode) { + if (this.timer) return + + this.timer = setTimeout(this.timerConfirmed.bind(this), 2000, vnode) + this.holding = true + }, + + timerStop() { + clearTimeout(this.timer) + this.timer = null + this.holding = false + }, + + timerConfirmed(vnode) { + this.timerStop() + vnode.attrs.onclick() + m.redraw() + }, + + view: function(vnode) { + return m('button.holdbutton', { + style: '--hold-bg: var(--error-bg); --hold-color: var(--error); --hold-fill: var(--error);', + hidden: vnode.attrs.hidden, + class: (vnode.attrs.class || '') + + (this.holding ? ' holdbutton-active' : ''), + onpointerdown: this.timerStart.bind(this, vnode), + onpointerup: this.timerStop.bind(this), + onpointerleave: this.timerStop.bind(this), + onkeydown: this.keydown.bind(this, vnode), + onkeyup: this.keyup.bind(this), + onclick: e => { return false }, + }, m('div.inner', vnode.attrs.text)) + }, +} + +module.exports = HoldButton diff --git a/filadelfia_web/app/index.js b/filadelfia_web/app/index.js index f39f7c6..a929e65 100644 --- a/filadelfia_web/app/index.js +++ b/filadelfia_web/app/index.js @@ -2,6 +2,7 @@ const m = require('mithril') const Authentication = require('./authentication') const Header = require('./header') const Login = require('./page_login') +const Logout = require('./page_logout') const Browse = require('./page_browse') const Upload = require('./page_upload') const Article = require('./page_article') @@ -36,12 +37,13 @@ m.route.link = function(vnode){ m.route.prefix = '' const allRoutes = { - '/': Login, - '/browse': Browse, + '/': Browse, + '/login': Login, + '/logout': Logout, '/upload': Upload, '/:year': Browse, '/:year/:month': Browse, - '/:year/:month/:id': Article, + '/:year/:month/:path': Article, } // Wait until we finish checking avif support, some views render immediately and will ask for this immediately before the callback gets called. diff --git a/filadelfia_web/app/input.js b/filadelfia_web/app/input.js index dd9b7cf..2214959 100644 --- a/filadelfia_web/app/input.js +++ b/filadelfia_web/app/input.js @@ -35,8 +35,17 @@ const Input = { } }, + onupdate: function(vnode) { + if (this.tempus && vnode.attrs.form[vnode.attrs.formKey]) { + if (vnode.attrs.form[vnode.attrs.formKey].getTime() !== this.tempus.viewDate?.getTime()) { + this.tempus.dates.setValue(new tempus.DateTime(vnode.attrs.form[vnode.attrs.formKey])) + } + } + }, + imageChanged: function(vnode, e) { - let file = vnode.attrs.form[vnode.attrs.formKey] = e.currentTarget.files?.[0] || null + let file = e.currentTarget.files?.[0] || null + this.updateValue(vnode, file) if (this.preview) { this.preview.clear() this.preview = null @@ -54,6 +63,14 @@ const Input = { } }, + updateValue: function(vnode, value) { + vnode.attrs.form[vnode.attrs.formKey] = value + if (typeof(vnode.attrs.oninput) === 'function') { + vnode.attrs.oninput(vnode.attrs.form[vnode.attrs.formKey]) + } + return false + }, + getInput: function(vnode) { switch (vnode.attrs.utility) { case 'file': @@ -68,7 +85,7 @@ const Input = { type: 'file', accept: vnode.attrs.accept, disabled: api.loading, - oninput: (e) => { vnode.attrs.form[vnode.attrs.formKey] = e.currentTarget.files?.[0] || null }, + oninput: (e) => this.updateValue(vnode, e.currentTarget.files?.[0] || null), }), ]) case 'datetime': @@ -78,12 +95,11 @@ const Input = { disabled: api.loading, oncreate: (e) => { this.tempus = new tempus.TempusDominus(e.dom, { - defaultDate: vnode.attrs.form[vnode.attrs.formKey], - viewDate: vnode.attrs.form[vnode.attrs.formKey], localization: tempusLocalization, }) + this.tempus.dates.setValue(new tempus.DateTime(vnode.attrs.form[vnode.attrs.formKey])) this.subscription = this.tempus.subscribe(tempus.Namespace.events.change, (e) => { - vnode.attrs.form[vnode.attrs.formKey] = e.date + this.updateValue(vnode, e.date) }); }, }), @@ -111,24 +127,14 @@ const Input = { disabled: api.loading, type: vnode.attrs.type || 'text', value: vnode.attrs.form[vnode.attrs.formKey], - oninput: (e) => { vnode.attrs.form[vnode.attrs.formKey] = e.currentTarget.value }, + oninput: (e) => this.updateValue(vnode, e.currentTarget.value), }) } }, view: function(vnode) { - let input = m('input', { - type: vnode.attrs.type || 'text', - value: vnode.attrs.form[vnode.attrs.formKey], - oninput: (e) => { vnode.attrs.form[vnode.attrs.formKey] = e.currentTarget.value }, - }) - - if (vnode.attrs.utility === 'datetime') { - - } - return [ - m('label', vnode.attrs.label), + vnode.attrs.label ? m('label', vnode.attrs.label) : null, this.getInput(vnode), ] }, diff --git a/filadelfia_web/app/lang.js b/filadelfia_web/app/lang.js index 9bebf0d..fdb6f1a 100644 --- a/filadelfia_web/app/lang.js +++ b/filadelfia_web/app/lang.js @@ -1,119 +1,150 @@ const out = { - currentlang: 'en', + currentlang: 'en', } const i18n = { - lang_change_long: ['Skipta yfir á íslensku', - 'Change to english'], - lang_current: ['en', - 'is'], - header_title: ['Fíladelfia archival center', - 'Fíladelfia myndhvelfing'], - header_logout: ['Log out', - 'Skrá út'], - title: ['Title', - 'Titill'], - date: ['Date', - 'Dagsetning'], - language: ['EN', - 'IS'], - upload_goto: ['Upload', - 'Upphlaða'], - login_error: ['Error while logging in: {0}', - 'Villa við innskráningu: {0}'], - login_error_auth: ['Unknown error from server. Try again later.', - 'Óþekkt villa frá vefþjóni. Reyndu aftur seinna.'], - login_missing_email: ['Email is missing', - 'Email eða nafn vantar'], - login_missing_password: ['Password is missing', - 'Lykilorð vantar'], - login_email: ['Email or name', - 'Email eða nafn'], - login_password: ['Password', - 'Lykilorð'], - login_submit: ['Log in', - 'Skrá inn'], - upload_missing_title: ['Title is missing', - 'Titill vantar'], - upload_missing_date: ['Date is missing', - 'Dagsetning vantar'], - upload_missing_file: ['Video file missing', - 'Myndaskrá vantar'], - upload_missing_banner: ['Poster image missing', - 'Mynd vantar'], - upload_error: ['Error while uploading: {0}', - 'Villa við að hlaða upp myndefni: {0}'], - unsplash: ['Photo by {0} on {1}', - 'Mynd eftir {0} frá {1}'], - api_down: ['No internet or browser blocked the request.', - 'Ekkert net eða vafri blockaði fyrirspurn.'], - months: { - '1': ['January', - 'Janúar'], - '2': ['February', - 'Febrúar'], - '3': ['March', - 'Mars'], - '4': ['April', - 'Apríl'], - '5': ['May', - 'Maí'], - '6': ['June', - 'Júní'], - '7': ['July', - 'Júlí'], - '8': ['August', - 'Ágúst'], - '9': ['September', - 'September'], - '10': ['Oktober', - 'Október'], - '11': ['November', - 'Nóvember'], - '12': ['December', - 'Desember'], - }, + lang_change_long: ['Skipta yfir á íslensku', + 'Change to english'], + lang_current: ['en', + 'is'], + header_title: ['Fíladelfia archival center', + 'Fíladelfia myndhvelfing'], + header_logout: ['Log out', + 'Skrá út'], + title: ['Title', + 'Titill'], + date: ['Date', + 'Dagsetning'], + language: ['EN', + 'IS'], + upload_goto: ['Upload', + 'Upphlaða'], + login_error: ['Error while logging in: {0}', + 'Villa við innskráningu: {0}'], + login_error_auth: ['Unknown error from server. Try again later.', + 'Óþekkt villa frá vefþjóni. Reyndu aftur seinna.'], + login_missing_email: ['Email is missing', + 'Email eða nafn vantar'], + login_missing_password:['Password is missing', + 'Lykilorð vantar'], + login_email: ['Email or name', + 'Email eða nafn'], + login_password: ['Password', + 'Lykilorð'], + login_submit: ['Log in', + 'Skrá inn'], + logout: ['Log out', + 'Skrá út'], + upload_missing_title: ['Title is missing', + 'Titill vantar'], + upload_missing_date: ['Date is missing', + 'Dagsetning vantar'], + upload_missing_file: ['Video file missing', + 'Myndaskrá vantar'], + upload_missing_banner: ['Poster image missing', + 'Mynd vantar'], + upload_error: ['Error while uploading: {0}', + 'Villa við að hlaða upp myndefni: {0}'], + unsplash: ['Photo by {0} on {1}', + 'Mynd eftir {0} frá {1}'], + api_down: ['No internet or browser blocked the request.', + 'Ekkert net eða vafri blockaði fyrirspurn.'], + edit: ['Edit', + 'Breyta'], + delete: ['Delete', + 'Eyða'], + article_speaker: ['Speaker', + 'Ræðumaður'], + delete_error: ['Error while deleting: {0}', + 'Villa við að eyða efni: {0}'], + article_error: ['Error while saving: {0}', + 'Villa við að vista: {0}'], + months: { + '1': ['January', + 'Janúar'], + '2': ['February', + 'Febrúar'], + '3': ['March', + 'Mars'], + '4': ['April', + 'Apríl'], + '5': ['May', + 'Maí'], + '6': ['June', + 'Júní'], + '7': ['July', + 'Júlí'], + '8': ['August', + 'Ágúst'], + '9': ['September', + 'September'], + '10': ['Oktober', + 'Október'], + '11': ['November', + 'Nóvember'], + '12': ['December', + 'Desember'], + }, } const langs = { - 'en': 0, - 'is': 1, + 'en': 0, + 'is': 1, } const regexNumber = new RegExp('^\\d+$') -out.langset = function(lang) { - out.currentlang = lang - let index = langs[lang] +out.printdate = function(date) { + let day = date.getDate().toString() + if (out.currentlang === 'en') { + let last = day[day.length - 1] + if (last === '1') { + day += 'st' + } else if (last === '2') { + day += 'nd' + } else if (last === '3') { + day += 'rd' + } else { + day += 'th' + } + } else { + day += '.' + } + return `${day} ${out.months[date.getMonth() + 1]} ${date.getFullYear()}, ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}` +} - for (let key of Object.keys(i18n)) { - if (!Array.isArray(i18n[key])) { - out[key] = {} - for (let subKey of Object.keys(i18n[key])) { - out[key][subKey] = i18n[key][subKey][index] - } - } else { - out[key] = i18n[key][index] - } - } +out.langset = function(lang) { + out.currentlang = lang + let index = langs[lang] + + for (let key of Object.keys(i18n)) { + if (!Array.isArray(i18n[key])) { + out[key] = {} + for (let subKey of Object.keys(i18n[key])) { + out[key][subKey] = i18n[key][subKey][index] + } + } else { + out[key] = i18n[key][index] + } + } } out.langtoggle = function() { - out.langset(out.currentlang === 'en' ? 'is' : 'en') - return false + out.langset(out.currentlang === 'en' ? 'is' : 'en') + return false } out.format = function(str, ...args) { - return out.mformat(str, ...args).join('') + return out.mformat(str, ...args).join('') } out.mformat = function(str, ...args) { - let split = (str || '').split(/\{|\}/) - return split.map(function(item) { - if (regexNumber.test(item)) { - return args[Number(item)] || item - } - return item - }) + let split = (str || '').split(/\{|\}/) + return split.map(function(item) { + if (regexNumber.test(item)) { + return args[Number(item)] || item + } + return item + }) } out.langset('is') diff --git a/filadelfia_web/app/page_article.js b/filadelfia_web/app/page_article.js index db6aaea..9b86639 100644 --- a/filadelfia_web/app/page_article.js +++ b/filadelfia_web/app/page_article.js @@ -3,14 +3,16 @@ const api = require('./api') const Authentication = require('./authentication') const Input = require('./input') const lang = require('./lang') +const videos = require('./videos') +const HoldButton = require('./holdbutton') const Article = { oninit: function(vnode) { - Authentication.requiresLogin() this.error = '' this.path = '' this.data = null this.editing = false + this.cacheImage = null this.form = { title: 'Sunnudagssamkoma', date: new Date(), @@ -23,7 +25,9 @@ const Article = { }, onbeforeupdate: function(vnode) { - let path = m.route.param('id') + let path = m.route.param('year').padStart(4, '0') + + '-' + m.route.param('month').padStart(2, '0') + + '-' + m.route.param('path') if (this.path === path) return this.fetchArticle(vnode, path) @@ -33,10 +37,12 @@ const Article = { this.error = '' this.data = null this.path = path + this.cacheImage = null + this.editing = false api.sendRequest({ method: 'GET', - url: '/api/auth/articles/' + this.path, + url: '/api/articles/' + this.path, }) .then((result) => { this.data = result.article @@ -58,74 +64,200 @@ const Article = { updatevideo: function(vnode, e) { this.error = '' + + if (!this.form.title) this.error = lang.upload_missing_title // Title is missing + if (!this.form.date) this.error = lang.upload_missing_date // Date is missing if (this.error) return false + + let promise = Promise.resolve() + + if (Authentication.currentUser && (typeof(this.form.banner) !== 'string' && this.cacheImage?.file !== this.form.banner)) { + promise = api.sendRequest({ + method: 'GET', + url: '/api/auth/uploadToken', + }) + .then(res => { + return api.uploadBanner(this.form.banner, res) + .then(imageData => { + this.cacheImage = imageData + + if (this.data.banner_path) { + api.sendRequest({ + method: 'DELETE', + url: res.delete + this.data.banner_path.slice(this.data.banner_path.lastIndexOf('/') + 1) + '?token=' + res.token, + }).catch(err => console.error(err)) + } + + return res + }) + }) + } + + promise.then(() => { + return api.sendRequest({ + method: 'PUT', + url: '/api/auth/articles/' + this.data.id, + body: { + name: this.form.title, + page_id: 'null', + path: this.form.date.toISOString().replace('T', '_').replace(/:/g, '').split('.')[0], + content: JSON.stringify(this.form.metadata), + publish_at: this.form.date, + admin_id: Authentication.getTokenDecoded().user_id, + is_featured: false, + media: null, + banner: this.cacheImage ? { + filename: this.cacheImage.medium.filename, + path: this.cacheImage.medium.path, + type: 'image/avif', + size: this.cacheImage.size, + preview: { + base64: this.cacheImage.preview.base64, + }, + } : null, + }, + }) + }) + .then(res => { + this.fetchArticle(vnode, this.path) + }) + .catch((error) => { + console.error(error) + this.error = lang.format(lang.article_error, error.message) // Error while saving: + }) + + + return false + }, + + deletevideo: function(vnode) { + api.sendRequest({ + method: 'GET', + url: '/api/auth/uploadToken', + body: this.form, + }) + .then(res => { + return Promise.all([ + this.data.banner_path ? api.sendRequest({ + method: 'DELETE', + url: res.delete + this.data.banner_path.slice(this.data.banner_path.lastIndexOf('/') + 1) + '?token=' + res.token, + }).catch(err => console.error(err)) : Promise.resolve(), + this.data.media_path ? api.sendRequest({ + method: 'DELETE', + url: res.delete + this.data.media_path.slice(this.data.media_path.lastIndexOf('/') + 1) + '?token=' + res.token, + }).catch(err => console.error(err)) : Promise.resolve(), + ]) + }) + .then(() => { + return api.sendRequest({ + method: 'DELETE', + url: '/api/auth/articles/' + this.data.id, + }) + }) + .then(res => { + videos.removeArticle(this.data.id) + m.route.set('/') + }) + .catch((error) => { + if (!error) return + + this.error = lang.format(lang.delete_error, error.message) // Error while uploading: + }) }, view: function(vnode) { - console.log(this.data) return [ - api.loading ? m('div.loading-spinner') : null, - this.error - ? m('div.full-error', { onclick: this.fetchArticle.bind(this, vnode, this.path) }, [ - this.error, m('br'), 'Click here to try again' - ]) - : null, - this.data?.media_path - ? [ - m('.player', [ - m('video', { - crossorigin: '', - controls: true, - preload: 'none', - poster: this.data.banner_path || '/assets/placeholder.avif', - }, [ - m('source', { - src: this.data.media_path - }) + api.loading && !this.data ? m('div.loading-spinner') : null, + this.data ? [ + this.data.media_path + ? [ + m('.player', [ + m('video', { + crossorigin: '', + controls: true, + preload: 'none', + poster: this.data.banner_path || '/assets/placeholder.avif', + }, [ + m('source', { + src: this.data.media_path + }) + ]), ]), + ] + : null, + this.editing + ? m('form.article', { + onsubmit: this.updatevideo.bind(this, vnode), + }, [ + m('div.form-row', [ + m('div.form-columns', [ + m(Input, { + label: '', + type: 'file', + accept: 'image/*', + utility: 'image', + form: this.form, + formKey: 'banner', + }), + ]), + m('div.form-columns.article-name', [ + m(Input, { + label: 'Title', + form: this.form, + formKey: 'title', + }), + m(Input, { + label: 'Date (dd.mm.yyyy)', + type: 'text', + utility: 'datetime', + form: this.form, + formKey: 'date', + }), + ]), + ]), + m('p.separator', 'Optional'), + m(Input, { + label: 'Speaker', + form: this.form.metadata, + formKey: 'speaker', + }), + this.error ? m('div.full-error', this.error) : null, + m('div.row', [ + m('input.spinner', { + hidden: api.loading, + type: 'submit', + value: lang.edit, + }), + m('div.filler', { + hidden: api.loading, + }), + api.loading ? m('div.loading-spinner') : null, + m(HoldButton, { + class: 'button spinner', + onclick: () => this.deletevideo(vnode), + hidden: api.loading, + text: lang.delete, + }), + ]), + ]) + : m('div.article', [ + m('h1', this.data.name), + m('p', [ + lang.printdate(this.form.date), + ]), + m('div.table', [ + m('div.table-row', [ + m('div.table-item', lang.article_speaker), + m('div.table-item', this.data.content.speaker || '...'), + ]), + ]), + Authentication.currentUser?.rank >= 10 + ? m('button.button', { onclick: () => this.editing = true },lang.edit) + : null, ]), - ] - : null, - this.editing - ? m('form.article', { - onsubmit: this.updatevideo.bind(this, vnode), - }, [ - m('div.form-row', [ - m('div.form-columns', [ - m(Input, { - label: 'Mynd', - type: 'file', - accept: 'image/*', - utility: 'image', - form: this.form, - formKey: 'banner', - }), - ]), - m('div.form-columns', [ - m(Input, { - label: 'Title', - form: this.form, - formKey: 'title', - }), - m(Input, { - label: 'Date (dd.mm.yyyy)', - type: 'text', - utility: 'datetime', - form: this.form, - formKey: 'date', - }), - ]), - ]), - m('p.separator', 'Optional'), - m(Input, { - label: 'Speaker', - form: this.form.metadata, - formKey: 'speaker', - }), - ]) - : m('div.article', [ - m('h1', this.data.name) - ]), + ] : [ + this.error ? m('div.full-error', this.error) : null, + ], ] }, } diff --git a/filadelfia_web/app/page_browse.js b/filadelfia_web/app/page_browse.js index fc98133..2a7f959 100644 --- a/filadelfia_web/app/page_browse.js +++ b/filadelfia_web/app/page_browse.js @@ -6,16 +6,15 @@ const lang = require('./lang') const Browse = { oninit: function(vnode) { - Authentication.requiresLogin() }, mArticles: function(vnode, articles) { return articles.map(article => { return m(m.route.Link, { - href: ['', article.publish_at.getFullYear(), article.publish_at.getMonth() + 1, article.id].join('/'), + href: ['', article.publish_at.getFullYear(), article.publish_at.getMonth() + 1, article.path_short].join('/'), style: article.avif_preview ? `background-image: url('${article.avif_preview}')` : null, }, [ - m('span', article.publish_at.toUTCString()), + m('span', lang.printdate(article.publish_at)), m('span', article.name), ]) }) @@ -48,7 +47,7 @@ const Browse = { ? this.mMonth(vnode, videos.year) : null, !videos.year - ? videos.Tree.slice(-2).map(year => { + ? videos.Tree.slice(-1).map(year => { return [ m('.gallery-year', year.title), this.mMonth(vnode, year), diff --git a/filadelfia_web/app/page_login.js b/filadelfia_web/app/page_login.js index 85023d3..6b62518 100644 --- a/filadelfia_web/app/page_login.js +++ b/filadelfia_web/app/page_login.js @@ -34,7 +34,7 @@ const Login = { .then((result) => { if (!result.token) return Promise.reject(new Error(lang.login_error_auth)) // Unknown error from server. Try again later Authentication.updateToken(result.token) - m.route.set(this.redirect || '/browse') + m.route.set(this.redirect || '/') videos.refreshTree() }) .catch((error) => { @@ -71,7 +71,6 @@ const Login = { value: lang.login_submit, // Log in }), api.loading ? m('div.loading-spinner') : null, - m('a', { onclick: lang.langtoggle }, lang.lang_change_long /* Skipta yfir á íslensku */), ]), ]), m('footer', lang.mformat( diff --git a/filadelfia_web/app/page_logout.js b/filadelfia_web/app/page_logout.js new file mode 100644 index 0000000..803dce7 --- /dev/null +++ b/filadelfia_web/app/page_logout.js @@ -0,0 +1,15 @@ +const m = require('mithril') +const Authentication = require('./authentication') + +const Logout = { + oninit: function(vnode) { + Authentication.clearToken() + m.route.set(vnode.attrs.redirect || '/') + }, + + view: function(vnode) { + return [] + }, +} + +module.exports = Logout diff --git a/filadelfia_web/app/page_upload.js b/filadelfia_web/app/page_upload.js index 28a896a..f9606d7 100644 --- a/filadelfia_web/app/page_upload.js +++ b/filadelfia_web/app/page_upload.js @@ -35,74 +35,23 @@ const Upload = { if (!this.form.title) this.error = lang.upload_missing_title // Title is missing if (!this.form.date) this.error = lang.upload_missing_date // Date is missing - // if (!this.form.file) this.error = lang.upload_missing_file // Video file missing - if (!this.form.banner) this.error = lang.upload_missing_banner // Video file missing + if (!this.form.file) this.error = lang.upload_missing_file // Video file missing + if (!this.form.banner) this.error = lang.upload_missing_banner // Poster image missing if (this.error) return false api.sendRequest({ method: 'GET', url: '/api/auth/uploadToken', - body: this.form, }) .then(res => { if (this.cacheImage?.file === this.form.banner) { return this.cacheImage } - var data = new FormData() - data.append('file', this.form.banner) - data.append('preview', JSON.stringify({ - "out": "base64", - "format": "avif", - "resize": { - "width": 360, - "height": 203, - "fit": "cover", - "withoutEnlargement": true, - "kernel": "mitchell" - }, - "avif": { - "quality": 50, - "effort": 9 - } - })) - data.append('medium', JSON.stringify({ - "format": "avif", - "resize": { - "width": 1280, - "height": 720, - "fit": "cover", - "withoutEnlargement": true, - "kernel": "mitchell" - }, - "avif": { - "quality": 75, - "effort": 3 - } - })) - - return api.sendRequest({ - method: 'POST', - url: res.resize + '?token=' + res.token, - body: data, - }) - .then(banner => { - this.cacheImage = { - file: this.form.banner, - medium: { - filename: banner.medium.filename, - path: banner.medium.path, - }, - preview: { - base64: banner.preview.base64, - }, - } - api.sendRequest({ - method: 'DELETE', - url: res.resize.replace('resize', banner.filename) + '?token=' + res.token, - }).catch(err => console.log(err)) - + return api.uploadBanner(this.form.banner, res) + .then(imageData => { + this.cacheImage = imageData return res }) }) @@ -151,7 +100,7 @@ const Upload = { filename: this.cacheImage.medium.filename, path: this.cacheImage.medium.path, type: 'image/avif', - size: this.form.file.size, + size: this.cacheImage.size, preview: { base64: this.cacheImage.preview.base64, }, @@ -160,9 +109,8 @@ const Upload = { }) }) .then(res => { - console.log(res) videos.refreshTree() - m.route.set('/browse') + m.route.set('/') }) .catch((error) => { this.uploading = null @@ -181,6 +129,22 @@ const Upload = { return false }, + filechanged(file) { + if (!file || !file.name) return + + let matches = /^(\d{4})-(\d\d)-(\d\d)_(\d\d)-(\d\d)/.exec(file.name) + if (!matches) return + + var date = new Date(matches.slice(1, 4).join('-') + 'T' + matches.slice(4,6).join(':') + ':00') + if (isNaN(date.getTime())) return + + if (date.getMinutes() >= 30 || date.getHours() === 10) { + date.setHours(date.getHours() + 1) + } + date.setMinutes(0) + this.form.date = date + }, + view: function(vnode) { return [ m('div.page.page-upload', [ @@ -210,6 +174,7 @@ const Upload = { button: 'fa-video', form: this.form, formKey: 'file', + oninput: (file) => this.filechanged(file), }), m(Input, { label: 'Mynd', @@ -242,8 +207,8 @@ const Upload = { ]), m('footer', lang.mformat( lang.unsplash, // Photo by X on Y - m('a', { href: 'https://unsplash.com/@guzmanbarquin?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Guzmán Barquín'), - m('a', { href: 'https://unsplash.com/photos/sea-under-full-moon-Qd688l1yDOI?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Unsplash'), + m('a', { href: 'https://unsplash.com/@franhotchin?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Francesca Hotchin'), + m('a', { href: 'https://unsplash.com/photos/landscape-photo-of-mountain-covered-with-snow-FN-cedy6NHA?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash', target: '_blank' }, 'Unsplash'), )), ]), ] diff --git a/filadelfia_web/app/videos.js b/filadelfia_web/app/videos.js index 535422a..9c13004 100644 --- a/filadelfia_web/app/videos.js +++ b/filadelfia_web/app/videos.js @@ -23,7 +23,7 @@ function calculateActiveBranches() { break } } - } else if (!path && (m.route.get() === '/' || (m.route.get() || '').startsWith('/browse'))) { + } else if (!path && m.route.get() === '/') { exports.year = Tree[Tree.length - 1] } else if (!path) { exports.year = null @@ -74,6 +74,14 @@ function rebuildTree() { } } +function removeArticle(id) { + let index = Articles.findIndex(article => article.id === id) + if (index >= 0) { + Articles.splice(index, 1) + rebuildTree() + } +} + function refreshTree() { exports.error = '' @@ -85,7 +93,7 @@ function refreshTree() { return api.sendRequest({ method: 'GET', - url: '/api/auth/articles', + url: '/api/articles', }) .then(result => { result.videos.forEach(video => { @@ -106,6 +114,7 @@ function refreshTree() { }) } +exports.removeArticle = removeArticle exports.rebuildTree = rebuildTree exports.refreshTree = refreshTree exports.calculateActiveBranches = calculateActiveBranches diff --git a/filadelfia_web/build-package.json b/filadelfia_web/build-package.json index f03ccc7..fd4ca28 100644 --- a/filadelfia_web/build-package.json +++ b/filadelfia_web/build-package.json @@ -1,8 +1,14 @@ { "scripts": { - "build": "echo done;" + "build": "esbuild app/index.js --bundle --outfile=public/assets/app.js" }, "dependencies": { + "@eonasdan/tempus-dominus": "^6.7.19", + "@popperjs/core": "^2.11.8", + "eltro": "^1.4.4", + "esbuild": "^0.19.5", + "flaska": "^1.3.2", + "mithril": "^2.2.2", "service-core": "^3.0.0-beta.17" } } diff --git a/filadelfia_web/package.json b/filadelfia_web/package.json index 38bb637..b79d489 100644 --- a/filadelfia_web/package.json +++ b/filadelfia_web/package.json @@ -1,6 +1,6 @@ { "name": "filadelfia_web", - "version": "1.0.0", + "version": "1.0.1", "port": 4130, "description": "Filadelfia web portal", "main": "index.js", diff --git a/filadelfia_web/public/assets/apple-touch-icon.png b/filadelfia_web/public/assets/apple-touch-icon.png new file mode 100644 index 0000000..c855881 Binary files /dev/null and b/filadelfia_web/public/assets/apple-touch-icon.png differ diff --git a/filadelfia_web/public/assets/bg.jpg b/filadelfia_web/public/assets/bg.jpg deleted file mode 100644 index c102c87..0000000 Binary files a/filadelfia_web/public/assets/bg.jpg and /dev/null differ diff --git a/filadelfia_web/public/assets/bg_admin.avif b/filadelfia_web/public/assets/bg_admin.avif deleted file mode 100644 index 616470e..0000000 Binary files a/filadelfia_web/public/assets/bg_admin.avif and /dev/null differ diff --git a/filadelfia_web/public/assets/favicon.png b/filadelfia_web/public/assets/favicon.png new file mode 100644 index 0000000..6b6226d Binary files /dev/null and b/filadelfia_web/public/assets/favicon.png differ diff --git a/filadelfia_web/public/assets/logo.svg b/filadelfia_web/public/assets/logo.svg new file mode 100644 index 0000000..2b41559 --- /dev/null +++ b/filadelfia_web/public/assets/logo.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/filadelfia_web/public/assets/logo_nobg.svg b/filadelfia_web/public/assets/logo_nobg.svg new file mode 100644 index 0000000..0d09d18 --- /dev/null +++ b/filadelfia_web/public/assets/logo_nobg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/filadelfia_web/public/assets/placeholder.jpg b/filadelfia_web/public/assets/placeholder.jpg deleted file mode 100644 index fab3633..0000000 Binary files a/filadelfia_web/public/assets/placeholder.jpg and /dev/null differ diff --git a/filadelfia_web/public/assets/placeholder.png b/filadelfia_web/public/assets/placeholder.png deleted file mode 100644 index f82ce68..0000000 Binary files a/filadelfia_web/public/assets/placeholder.png and /dev/null differ diff --git a/filadelfia_web/public/favicon.ico b/filadelfia_web/public/favicon.ico new file mode 100644 index 0000000..83e5dbc Binary files /dev/null and b/filadelfia_web/public/favicon.ico differ diff --git a/filadelfia_web/public/index.html b/filadelfia_web/public/index.html index c7e8fdb..5abe792 100644 --- a/filadelfia_web/public/index.html +++ b/filadelfia_web/public/index.html @@ -5,500 +5,193 @@ Filadelfia web portal - + +