Compare commits
23 Commits
discord_em
...
master
Author | SHA1 | Date |
---|---|---|
Jonatan Nilsson | 852f37dc8d | |
Jonatan Nilsson | 14a3bc3123 | |
Jonatan Nilsson | ab2e7b93c4 | |
Jonatan Nilsson | 2e7b3be8d5 | |
Jonatan Nilsson | 533e279b0b | |
Jonatan Nilsson | e75719682e | |
Jonatan Nilsson | c55f0c9a02 | |
Jonatan Nilsson | 0b686a462f | |
Jonatan Nilsson | 329a3e267c | |
Jonatan Nilsson | ab9ed32196 | |
Jonatan Nilsson | 3a6996bfbb | |
TheThing | bdeeff3794 | |
Jonatan Nilsson | fad7acd5f7 | |
Jonatan Nilsson | 2d7101d666 | |
Jonatan Nilsson | 16b87aabcf | |
Jonatan Nilsson | 857a087410 | |
TheThing | ec7ade938f | |
Jonatan Nilsson | 638e6cc435 | |
Jonatan Nilsson | 531c7acefe | |
TheThing | a5c7e53802 | |
TheThing | d1730974dc | |
TheThing | 4216e036e4 | |
TheThing | 825cd7ef2d |
|
@ -8,11 +8,14 @@ export default class ArticleRoutes {
|
|||
uploadMedia: uploadMedia,
|
||||
uploadFile: uploadFile,
|
||||
deleteFile: deleteFile,
|
||||
requireAuth: opts.requireAuth,
|
||||
})
|
||||
}
|
||||
|
||||
register(server) {
|
||||
server.flaska.get('/api/articles/:path', this.getArticle.bind(this))
|
||||
if (!this.requireAuth) {
|
||||
server.flaska.get('/api/articles/:path', this.getArticle.bind(this))
|
||||
}
|
||||
server.flaska.get('/api/auth/articles', server.authenticate(), this.auth_getAllArticles.bind(this))
|
||||
server.flaska.get('/api/auth/articles/:id', server.authenticate(), this.auth_getSingleArticle.bind(this))
|
||||
server.flaska.put('/api/auth/articles/:id', [
|
||||
|
@ -74,6 +77,7 @@ export default class ArticleRoutes {
|
|||
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
|
||||
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
|
||||
}
|
||||
|
||||
let res = await ctx.db.safeCallProc('article_auth_get_update_create', params)
|
||||
|
||||
ctx.body = this.private_getUpdateArticle_resOutput(res)
|
||||
|
@ -111,6 +115,10 @@ export default class ArticleRoutes {
|
|||
)
|
||||
}
|
||||
|
||||
if (ctx.req.body.media && ctx.req.body.media.filename && ctx.req.body.media.type && ctx.req.body.media.path && ctx.req.body.media.size) {
|
||||
newMedia = ctx.req.body.media
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
return this.private_getUpdateArticle(ctx, ctx.req.body, newBanner, newMedia)
|
||||
|
|
|
@ -18,8 +18,8 @@ export default class AuthenticationRoutes {
|
|||
/** GET: /api/authentication/login */
|
||||
async login(ctx) {
|
||||
let res = await ctx.db.safeCallProc('auth_login', [
|
||||
ctx.req.body.email,
|
||||
ctx.req.body.password,
|
||||
ctx.req.body.email || '',
|
||||
ctx.req.body.password || '',
|
||||
])
|
||||
|
||||
let out = res.results[0][0]
|
||||
|
|
|
@ -53,6 +53,7 @@ nconf.defaults({
|
|||
"iss": "dev",
|
||||
"path": "https://media.nfp.is/media/resize",
|
||||
"filePath": "https://media.nfp.is/media",
|
||||
"directFilePath": "https://media.nfp.is/media/noprefix",
|
||||
"removePath": "https://media.nfp.is/media/",
|
||||
"preview": {
|
||||
"out": "base64",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import MSSQL from 'msnodesqlv8'
|
||||
import { HttpError } from 'flaska'
|
||||
import { HttpErrorInternal } from './error.mjs'
|
||||
|
||||
export function initPool(core, config) {
|
||||
let pool = new MSSQL.Pool(config)
|
||||
|
@ -32,7 +33,10 @@ export function initPool(core, config) {
|
|||
if (err.lineNumber && err.procName) {
|
||||
message = `Error at ${err.procName}:${err.lineNumber} => ${message}`
|
||||
}
|
||||
throw new HttpError(500, message)
|
||||
throw new HttpErrorInternal(message, err, !err.lineNumber ? {
|
||||
name,
|
||||
params
|
||||
} : null)
|
||||
})
|
||||
},
|
||||
promises: pool.promises,
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { HttpError } from 'flaska'
|
||||
|
||||
export class HttpErrorInternal extends HttpError {
|
||||
constructor(message, inner, extra) {
|
||||
super(500, message);
|
||||
|
||||
Error.captureStackTrace(this, HttpError);
|
||||
|
||||
let proto = Object.getPrototypeOf(this);
|
||||
proto.name = 'HttpErrorInternal';
|
||||
|
||||
this.inner = inner
|
||||
this.extra = extra
|
||||
}
|
||||
}
|
|
@ -98,6 +98,10 @@ export default class Client {
|
|||
}
|
||||
|
||||
createJwt(body, secret) {
|
||||
return Client.createJwt(body, secret)
|
||||
}
|
||||
|
||||
static createJwt(body, secret) {
|
||||
let header = {
|
||||
typ: 'JWT',
|
||||
alg: 'HS256',
|
||||
|
|
|
@ -15,8 +15,6 @@ export function uploadMedia(file) {
|
|||
}
|
||||
}
|
||||
|
||||
console.log(media)
|
||||
|
||||
let body = {}
|
||||
|
||||
if (media.preview) {
|
||||
|
|
|
@ -5,9 +5,9 @@ export function mediaToDatabase(media, removeFlag) {
|
|||
media.type,
|
||||
media.path,
|
||||
media.size,
|
||||
media.preview.base64,
|
||||
media.sizes.small.avif.path.replace(/_small\.avif$/, ''),
|
||||
JSON.stringify(media.sizes),
|
||||
media.preview?.base64 || null,
|
||||
media.sizes?.small?.avif?.path?.replace(/_small\.avif$/, '') || null,
|
||||
JSON.stringify(media.sizes || {}),
|
||||
removeFlag ? 1 : 0,
|
||||
]
|
||||
} else {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Flaska, QueryHandler, JsonHandler, FormidableHandler } from 'flaska'
|
||||
import { Flaska, QueryHandler, JsonHandler, FormidableHandler, HttpError } from 'flaska'
|
||||
import formidable from 'formidable'
|
||||
|
||||
import config from './config.mjs'
|
||||
|
@ -48,6 +48,23 @@ export default class Server {
|
|||
this.flaska.devMode()
|
||||
}
|
||||
|
||||
this.flaska.onerror((err, ctx) => {
|
||||
if (err instanceof HttpError && err.status !== 500) {
|
||||
ctx.status = err.status
|
||||
ctx.log.warn(err.message)
|
||||
} else {
|
||||
ctx.log.error(err.inner || err)
|
||||
if (err.extra) {
|
||||
ctx.log.error({ extra: err.extra }, 'Database parameters')
|
||||
}
|
||||
ctx.status = 500
|
||||
}
|
||||
ctx.body = {
|
||||
status: ctx.status,
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
|
||||
this.flaska.before(function(ctx) {
|
||||
ctx.state.started = new Date().getTime()
|
||||
ctx.req.ip = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress
|
||||
|
|
|
@ -23,11 +23,11 @@ export function decode(base64StringUrlSafe) {
|
|||
export function parseMediaAndBanner(item) {
|
||||
if (item.banner_path) {
|
||||
item.banner_path = 'https://cdn.nfp.is' + item.banner_path
|
||||
item.banner_alt_prefix = 'https://cdn.nfp.is' + item.banner_alt_prefix
|
||||
item.banner_alt_prefix = 'https://cdn.nfp.is' + (item.banner_alt_prefix || '')
|
||||
}
|
||||
if (item.media_path) {
|
||||
item.media_path = 'https://cdn.nfp.is' + item.media_path
|
||||
item.media_alt_prefix = 'https://cdn.nfp.is' + item.media_alt_prefix
|
||||
item.media_alt_prefix = 'https://cdn.nfp.is' + (item.media_alt_prefix || '')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "discord_embed",
|
||||
"version": "1.0.25",
|
||||
"version": "1.0.26",
|
||||
"port": 4120,
|
||||
"description": "AV1 discord server embed helper",
|
||||
"main": "index.js",
|
||||
|
|
|
@ -232,7 +232,7 @@ pre {
|
|||
<p>Your link is:</p>
|
||||
<pre>{{=siteUrl}}</pre>
|
||||
<div class="center">
|
||||
<video id=mainplayer" controls poster="{{=imageLink}}">
|
||||
<video id=mainplayer" crossorigin controls poster="{{=imageLink}}">
|
||||
<source src="{{=videoLink}}">
|
||||
</video>
|
||||
</div>
|
||||
|
@ -268,7 +268,7 @@ pre {
|
|||
<p>Test area (does the video load and play? If not, it might not work on discord)</p>
|
||||
|
||||
<div class="center">
|
||||
<video class="hidden" id="previewVideo" controls poster="" preload="none">
|
||||
<video class="hidden" id="previewVideo" crossorigin controls poster="" preload="none">
|
||||
<source src="">
|
||||
</video>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
|
@ -0,0 +1,52 @@
|
|||
import config from '../../base/config.mjs'
|
||||
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 = {}) {
|
||||
opts.requireAuth = true
|
||||
super(opts)
|
||||
|
||||
Object.assign(this, {
|
||||
uploadFile: uploadFile,
|
||||
deleteFile: deleteFile,
|
||||
})
|
||||
}
|
||||
|
||||
register(server) {
|
||||
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/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_get_all', [])
|
||||
|
||||
ctx.body = {
|
||||
videos: parseVideos(res.results[0]),
|
||||
}
|
||||
}
|
||||
|
||||
/** GET: /api/auth/uploadToken */
|
||||
async getUploadToken(ctx) {
|
||||
const media = config.get('media')
|
||||
|
||||
ctx.body = {
|
||||
token: Client.createJwt({ iss: media.iss }, media.secret),
|
||||
path: config.get('media:directFilePath'),
|
||||
resize: config.get('media:path'),
|
||||
delete: config.get('media:removePath'),
|
||||
}
|
||||
}
|
||||
|
||||
/** PUT: /api/auth/articles/:id */
|
||||
async updateCreateArticle(ctx) {
|
||||
return this.private_getUpdateArticle(ctx, ctx.req.body, ctx.req.body.banner, ctx.req.body.media)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
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)
|
||||
video.metadata = JSON.parse(video.metadata || '{}')
|
||||
delete video.banner_alt_prefix
|
||||
delete video.media_alt_prefix
|
||||
return video
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -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 ArticleRoutes from './article/routes.mjs'
|
||||
|
||||
export default class Server extends Parent {
|
||||
init() {
|
||||
super.init()
|
||||
let localUtil = new this.core.sc.Util(import.meta.url)
|
||||
|
||||
this.flaskaOptions.appendHeaders['Content-Security-Policy'] = `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; connect-src 'self' https://media.nfp.is/; media-src 'self' https://cdn.nfp.is/`,
|
||||
this.flaskaOptions.nonce = []
|
||||
this.routes = {
|
||||
static: new StaticRoutes(),
|
||||
auth: new AuthenticationRoutes(),
|
||||
article: new ArticleRoutes(),
|
||||
}
|
||||
this.routes.serve = new ServeHandler({
|
||||
root: localUtil.getPathFromRoot('../public'),
|
||||
version: this.core.app.running,
|
||||
frontend: config.get('frontend:url'),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
const Authentication = require('./authentication')
|
||||
const lang = require('./lang')
|
||||
|
||||
function safeParseReponse(str, status, url) {
|
||||
if (status === 0) {
|
||||
return new Error(lang.api_down)
|
||||
}
|
||||
if (str.slice(0, 9) === '<!doctype') {
|
||||
if (status === 500) {
|
||||
return new Error('Server is temporarily down, try again later.')
|
||||
}
|
||||
return new Error('Expected JSON but got HTML (' + status + ': ' + url.split('?')[0] + ')')
|
||||
}
|
||||
if (!str) {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (err) {
|
||||
return new Error('Unexpected non-JSON response: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
let requests = new Set()
|
||||
let requestIndex = 0
|
||||
|
||||
function clearLoading(request) {
|
||||
requests.delete(request)
|
||||
if (!requests.size) {
|
||||
api.loading = false
|
||||
window.requestAnimationFrame(m.redraw.bind(m))
|
||||
}
|
||||
}
|
||||
|
||||
const api = {
|
||||
loading: false,
|
||||
sendRequest: function(options, isPagination) {
|
||||
let request = requestIndex++
|
||||
requests.add(request)
|
||||
api.loading = true
|
||||
let token = Authentication.getToken()
|
||||
let pagination = isPagination
|
||||
|
||||
if (token) {
|
||||
options.headers = options.headers || {}
|
||||
options.headers['Authorization'] = 'Bearer ' + token
|
||||
}
|
||||
|
||||
options.extract = function(xhr) {
|
||||
let out = safeParseReponse(xhr.responseText, xhr.status, this.url)
|
||||
|
||||
if (out instanceof Error) {
|
||||
throw out
|
||||
}
|
||||
if (xhr.status >= 300) {
|
||||
if (out.message) {
|
||||
throw out
|
||||
}
|
||||
console.error('Got error ' + xhr.status + ' but no error message:', out)
|
||||
throw new Error('Unknown or empty response from server.')
|
||||
}
|
||||
if (pagination) {
|
||||
let headers = {}
|
||||
|
||||
xhr.getAllResponseHeaders().split('\r\n').forEach(function(item) {
|
||||
let splitted = item.split(': ')
|
||||
headers[splitted[0]] = splitted[1]
|
||||
})
|
||||
|
||||
out = {
|
||||
headers: headers || {},
|
||||
data: out,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
return m.request(options)
|
||||
.then(function(res) {
|
||||
clearLoading(request)
|
||||
return res
|
||||
})
|
||||
.catch(function (error) {
|
||||
clearLoading(request)
|
||||
window.requestAnimationFrame(m.redraw.bind(m))
|
||||
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)
|
||||
})
|
||||
},
|
||||
|
||||
uploadBanner: function(bannerFile, token, reporter) {
|
||||
let request = requestIndex++
|
||||
requests.add(request)
|
||||
api.loading = true
|
||||
|
||||
var report = reporter || function() {}
|
||||
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": 40,
|
||||
"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
|
||||
}
|
||||
}))
|
||||
|
||||
report(lang.api_banner_upload)
|
||||
|
||||
return api.sendRequest({
|
||||
method: 'POST',
|
||||
url: token.resize + '?token=' + token.token,
|
||||
body: data,
|
||||
})
|
||||
.then(async (banner) => {
|
||||
let preview = null
|
||||
let quality = 60
|
||||
while (!preview && quality > 10) {
|
||||
report(lang.format(lang.api_banner_generate, quality))
|
||||
let check = await api.sendRequest({
|
||||
method: 'POST',
|
||||
url: token.resize + '/' + banner.filename + '?token=' + token.token,
|
||||
body: {
|
||||
"preview": {
|
||||
"out": "base64",
|
||||
"format": "avif",
|
||||
"resize": {
|
||||
"width": 360,
|
||||
"height": 203,
|
||||
"fit": "cover",
|
||||
"withoutEnlargement": true,
|
||||
"kernel": "mitchell"
|
||||
},
|
||||
"avif": {
|
||||
"quality": quality,
|
||||
"effort": 9
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (check.preview.base64.length < 8000) {
|
||||
preview = check.preview.base64
|
||||
} else {
|
||||
quality -= 5
|
||||
}
|
||||
}
|
||||
report(null)
|
||||
|
||||
api.sendRequest({
|
||||
method: 'DELETE',
|
||||
url: token.delete + banner.filename + '?token=' + token.token,
|
||||
}).catch(err => console.error(err))
|
||||
|
||||
return {
|
||||
file: bannerFile,
|
||||
size: bannerFile.size,
|
||||
medium: {
|
||||
filename: banner.medium.filename,
|
||||
path: banner.medium.path,
|
||||
},
|
||||
preview: {
|
||||
base64: preview,
|
||||
},
|
||||
}
|
||||
})
|
||||
.then(
|
||||
function(res) {
|
||||
clearLoading(request)
|
||||
return res
|
||||
},
|
||||
function(err) {
|
||||
clearLoading(request)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
prettyFormatBytes: function(bytes) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`
|
||||
}
|
||||
if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`
|
||||
}
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
},
|
||||
|
||||
uploadFileProgress: function(options, file, reporter) {
|
||||
let request = requestIndex++
|
||||
requests.add(request)
|
||||
api.loading = true
|
||||
|
||||
return new Promise(function(res, rej) {
|
||||
let report = reporter || function() {}
|
||||
let formdata = new FormData()
|
||||
|
||||
formdata.append('file', file)
|
||||
|
||||
let request = new XMLHttpRequest()
|
||||
|
||||
let finished = false
|
||||
let lastMarker = new Date()
|
||||
let lastMarkerLoaded = 0
|
||||
let lastMarkerSpeed = '...'
|
||||
|
||||
request.abortRequest = function() {
|
||||
finished = true
|
||||
request.abort()
|
||||
}
|
||||
|
||||
request.upload.addEventListener('progress', function (e) {
|
||||
let check = new Date()
|
||||
if (check - lastMarker >= 1000) {
|
||||
let loaded = e.loaded - lastMarkerLoaded
|
||||
lastMarkerSpeed = api.prettyFormatBytes(loaded / ((check - lastMarker) / 1000))
|
||||
lastMarker = check
|
||||
lastMarkerLoaded = e.loaded
|
||||
}
|
||||
report(request, Math.min(e.loaded / file.size * 100, 100), lastMarkerSpeed)
|
||||
})
|
||||
request.addEventListener('abort', function(e) {
|
||||
finished = true
|
||||
window.requestAnimationFrame(m.redraw.bind(m))
|
||||
rej()
|
||||
})
|
||||
|
||||
request.addEventListener('readystatechange', function(e) {
|
||||
if (finished) return
|
||||
if (request.readyState !== 4) return
|
||||
|
||||
finished = true
|
||||
let out = safeParseReponse(request.responseText, request.status, options.url)
|
||||
if (out instanceof Error || request.status >= 300) {
|
||||
return rej(out)
|
||||
}
|
||||
return res(out)
|
||||
})
|
||||
|
||||
request.open(options.method || 'POST', options.url)
|
||||
request.send(formdata)
|
||||
|
||||
report(request, 0)
|
||||
})
|
||||
.then(function(res) {
|
||||
clearLoading(request)
|
||||
return res
|
||||
}, function(err) {
|
||||
clearLoading(request)
|
||||
return Promise.reject(err)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = api
|
|
@ -0,0 +1,64 @@
|
|||
const m = require('mithril')
|
||||
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)
|
||||
},
|
||||
|
||||
getTokenDecoded: function() {
|
||||
let token = Authentication.getToken()
|
||||
var base64Url = token.split('.')[1];
|
||||
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
},
|
||||
|
||||
requiresLogin: function() {
|
||||
if (Authentication.currentUser) return
|
||||
m.route.set('/login')
|
||||
},
|
||||
|
||||
requiresNotLogin: function() {
|
||||
if (!Authentication.currentUser) return
|
||||
m.route.set('/')
|
||||
},
|
||||
}
|
||||
|
||||
Authentication.updateToken(localStorage.getItem(storageName))
|
||||
|
||||
window.Authentication = Authentication
|
||||
|
||||
module.exports = Authentication
|
|
@ -0,0 +1,72 @@
|
|||
const m = require('mithril')
|
||||
const videos = require('./videos')
|
||||
const Authentication = require('./authentication')
|
||||
const lang = require('./lang')
|
||||
|
||||
const Menu = {
|
||||
oninit: function(vnode) {
|
||||
this.currentActive = 'home'
|
||||
this.loading = false
|
||||
if (!videos.Tree.length) {
|
||||
videos.refreshTree()
|
||||
}
|
||||
this.onbeforeupdate()
|
||||
},
|
||||
|
||||
onbeforeupdate: function() {
|
||||
videos.calculateActiveBranches()
|
||||
let currentPath = m.route.get()
|
||||
},
|
||||
|
||||
logOut: function() {
|
||||
Authentication.clearToken()
|
||||
m.route.set('/')
|
||||
},
|
||||
|
||||
view: function() {
|
||||
let tree = videos.Tree
|
||||
let last = videos.Tree[videos.Tree.length - 1]
|
||||
let hasId = m.route.param('id')
|
||||
|
||||
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.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,
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = Menu
|
|
@ -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
|
|
@ -0,0 +1,62 @@
|
|||
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')
|
||||
window.m = m
|
||||
|
||||
let css = [
|
||||
'/assets/app.css?v=2',
|
||||
'/assets/tempus-dominus.css',
|
||||
'/assets/fontawesome.css',
|
||||
]
|
||||
|
||||
for (let item of css) {
|
||||
var fileref = document.createElement("link");
|
||||
fileref.setAttribute("rel", "stylesheet");
|
||||
fileref.setAttribute("type", "text/css");
|
||||
fileref.setAttribute("href", item);
|
||||
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 = {
|
||||
'/': Browse,
|
||||
'/login': Login,
|
||||
'/logout': Logout,
|
||||
'/upload': Upload,
|
||||
'/:year': Browse,
|
||||
'/:year/:month': Browse,
|
||||
'/: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.
|
||||
|
||||
/*
|
||||
* 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=';
|
|
@ -0,0 +1,143 @@
|
|||
const m = require('mithril')
|
||||
const api = require('./api')
|
||||
const tempus = require('@eonasdan/tempus-dominus')
|
||||
|
||||
const tempusLocalization = {
|
||||
locale: 'is',
|
||||
startOfTheWeek: 0,
|
||||
hourCycle: 'h23',
|
||||
dateFormats: {
|
||||
LTS: 'H:mm:ss',
|
||||
LT: 'H:mm',
|
||||
L: 'dd.MM.yyyy',
|
||||
LL: 'd [de] MMMM [de] yyyy',
|
||||
LLL: 'd [de] MMMM [de] yyyy H:mm',
|
||||
LLLL: 'dddd, d [de] MMMM [de] yyyy H:mm',
|
||||
},
|
||||
}
|
||||
|
||||
const Input = {
|
||||
oninit: function(vnode) {
|
||||
this.tempus = null
|
||||
this.subscription = null
|
||||
this.input = null
|
||||
this.preview = null
|
||||
},
|
||||
|
||||
onremove: function(vnode) {
|
||||
if (this.subscription) this.subscription.unsubscribe()
|
||||
if (this.tempus) {
|
||||
this.tempus.dispose()
|
||||
this.tempus = null
|
||||
}
|
||||
if (this.preview) {
|
||||
this.preview.clear()
|
||||
}
|
||||
},
|
||||
|
||||
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 = e.currentTarget.files?.[0] || null
|
||||
this.updateValue(vnode, file)
|
||||
if (this.preview) {
|
||||
this.preview.clear()
|
||||
this.preview = null
|
||||
}
|
||||
if (!file) return
|
||||
|
||||
if (file.type.startsWith('image')) {
|
||||
this.preview = {
|
||||
file: file,
|
||||
preview: URL.createObjectURL(file),
|
||||
clear: function() {
|
||||
URL.revokeObjectURL(this.preview)
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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':
|
||||
return m('div.form-row', [
|
||||
m('input', {
|
||||
type: 'text',
|
||||
disabled: api.loading,
|
||||
value: vnode.attrs.form[vnode.attrs.formKey]?.name || '',
|
||||
}),
|
||||
m('button.fal', { class: vnode.attrs.button || 'file' }),
|
||||
m('input.cover', {
|
||||
type: 'file',
|
||||
accept: vnode.attrs.accept,
|
||||
disabled: api.loading,
|
||||
oninput: (e) => this.updateValue(vnode, e.currentTarget.files?.[0] || null),
|
||||
}),
|
||||
])
|
||||
case 'datetime':
|
||||
return m('div.form-row', [
|
||||
m('input', {
|
||||
type: 'text',
|
||||
disabled: api.loading,
|
||||
oncreate: (e) => {
|
||||
this.tempus = new tempus.TempusDominus(e.dom, {
|
||||
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) => {
|
||||
this.updateValue(vnode, e.date)
|
||||
});
|
||||
},
|
||||
}),
|
||||
m('button.fal.fa-calendar', {
|
||||
onclick: () => { this.tempus.toggle(); return false },
|
||||
})
|
||||
])
|
||||
case 'image':
|
||||
let imageLink = this.preview && this.preview.preview || vnode.attrs.form[vnode.attrs.formKey]
|
||||
|
||||
return m('div.form-row.image-banner', {
|
||||
style: {
|
||||
'background-image': typeof imageLink === 'string' ? 'url("' + (imageLink) + '")' : null,
|
||||
},
|
||||
}, [
|
||||
m('input.cover', {
|
||||
type: 'file',
|
||||
accept: vnode.attrs.accept,
|
||||
disabled: api.loading,
|
||||
onchange: this.imageChanged.bind(this, vnode),
|
||||
}),
|
||||
])
|
||||
default:
|
||||
return m('input', {
|
||||
disabled: api.loading,
|
||||
type: vnode.attrs.type || 'text',
|
||||
value: vnode.attrs.form[vnode.attrs.formKey],
|
||||
oninput: (e) => this.updateValue(vnode, e.currentTarget.value),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
return [
|
||||
vnode.attrs.label ? m('label', vnode.attrs.label) : null,
|
||||
this.getInput(vnode),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = Input
|
|
@ -0,0 +1,156 @@
|
|||
const out = {
|
||||
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'],
|
||||
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}'],
|
||||
api_banner_upload: ['Uploading banner image',
|
||||
'Er að senda mynd'],
|
||||
api_banner_generate:['Generating preview, testing quality {0}%',
|
||||
'Bý til forsíðumynd, prufa {0}% gæði'],
|
||||
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,
|
||||
}
|
||||
|
||||
const regexNumber = new RegExp('^\\d+$')
|
||||
|
||||
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')}`
|
||||
}
|
||||
|
||||
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.format = function(str, ...args) {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
out.langset('is')
|
||||
|
||||
module.exports = out
|
|
@ -0,0 +1,265 @@
|
|||
const m = require('mithril')
|
||||
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) {
|
||||
this.error = ''
|
||||
this.path = ''
|
||||
this.data = null
|
||||
this.editing = false
|
||||
this.cacheImage = null
|
||||
this.form = {
|
||||
title: 'Sunnudagssamkoma',
|
||||
date: new Date(),
|
||||
banner: null,
|
||||
metadata: {
|
||||
speaker: '',
|
||||
},
|
||||
}
|
||||
this.onbeforeupdate(vnode)
|
||||
},
|
||||
|
||||
onbeforeupdate: function(vnode) {
|
||||
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)
|
||||
},
|
||||
|
||||
fetchArticle: function(vnode, path) {
|
||||
this.error = ''
|
||||
this.data = null
|
||||
this.path = path
|
||||
this.cacheImage = null
|
||||
this.editing = false
|
||||
|
||||
api.sendRequest({
|
||||
method: 'GET',
|
||||
url: '/api/articles/' + this.path,
|
||||
})
|
||||
.then((result) => {
|
||||
this.data = result.article
|
||||
this.gotArticle(vnode)
|
||||
}, (err) => {
|
||||
this.error = err.message
|
||||
})
|
||||
},
|
||||
|
||||
gotArticle: function(vnode) {
|
||||
if (!this.data) {
|
||||
return this.error = 'Article not found'
|
||||
}
|
||||
this.form.title = this.data.name
|
||||
this.form.date = new Date(this.data.publish_at)
|
||||
this.form.banner = this.data.banner_path
|
||||
this.form.metadata.speaker = this.data.content.speaker
|
||||
},
|
||||
|
||||
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) {
|
||||
return [
|
||||
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,
|
||||
]),
|
||||
] : [
|
||||
this.error ? m('div.full-error', this.error) : null,
|
||||
],
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = Article
|
|
@ -0,0 +1,62 @@
|
|||
const m = require('mithril')
|
||||
const api = require('./api')
|
||||
const Authentication = require('./authentication')
|
||||
const videos = require('./videos')
|
||||
const lang = require('./lang')
|
||||
|
||||
const Browse = {
|
||||
oninit: function(vnode) {
|
||||
},
|
||||
|
||||
mArticles: function(vnode, articles) {
|
||||
return articles.map(article => {
|
||||
return m(m.route.Link, {
|
||||
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', lang.printdate(article.publish_at)),
|
||||
m('span', article.name),
|
||||
])
|
||||
})
|
||||
},
|
||||
|
||||
mMonth: function(vnode, year) {
|
||||
return year.branches.map(month => {
|
||||
return [
|
||||
m('.gallery-month', lang.months[month.title]),
|
||||
m('.group', this.mArticles(vnode, month.videos))
|
||||
]
|
||||
})
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
let articles = videos.month?.videos || videos.year?.videos || videos.Articles
|
||||
|
||||
return [
|
||||
api.loading ? m('div.loading-spinner') : null,
|
||||
videos.error
|
||||
? m('div.full-error', { onclick: videos.refreshTree }, [
|
||||
videos.error, m('br'), 'Click here to try again'
|
||||
])
|
||||
: null,
|
||||
m('.gallery', [
|
||||
videos.month
|
||||
? m('.group', this.mArticles(vnode, articles))
|
||||
: null,
|
||||
videos.year && !videos.month
|
||||
? this.mMonth(vnode, videos.year)
|
||||
: null,
|
||||
!videos.year
|
||||
? videos.Tree.slice(-1).map(year => {
|
||||
return [
|
||||
m('.gallery-year', year.title),
|
||||
this.mMonth(vnode, year),
|
||||
]
|
||||
})
|
||||
: null
|
||||
]),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = Browse
|
|
@ -0,0 +1,86 @@
|
|||
const m = require('mithril')
|
||||
const Authentication = require('./authentication')
|
||||
const api = require('./api')
|
||||
const Input = require('./input')
|
||||
const lang = require('./lang')
|
||||
const videos = require('./videos')
|
||||
|
||||
const Login = {
|
||||
oninit: function(vnode) {
|
||||
this.redirect = vnode.attrs.redirect || ''
|
||||
Authentication.requiresNotLogin()
|
||||
|
||||
this.error = ''
|
||||
this.form = {
|
||||
email: '',
|
||||
password: '',
|
||||
}
|
||||
},
|
||||
|
||||
loginuser: function(vnode, e) {
|
||||
e.preventDefault()
|
||||
this.error = ''
|
||||
|
||||
if (!this.form.email) this.error = lang.login_missing_email // Email is missing
|
||||
if (!this.form.password) this.error = lang.login_missing_password // Password is missing
|
||||
|
||||
if (this.error) return false
|
||||
|
||||
api.sendRequest({
|
||||
method: 'POST',
|
||||
url: '/api/authentication/login',
|
||||
body: this.form,
|
||||
})
|
||||
.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 || '/')
|
||||
videos.refreshTree()
|
||||
})
|
||||
.catch((error) => {
|
||||
this.error = lang.format(lang.login_error, error.message) // Error while logging in:
|
||||
this.form.password = ''
|
||||
})
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
return [
|
||||
m('div.page.page-login', [
|
||||
m('div.modal', [
|
||||
m('form', {
|
||||
onsubmit: this.loginuser.bind(this, vnode),
|
||||
}, [
|
||||
m('h3', lang.header_title /* Filadelfia archival center */),
|
||||
this.error ? m('p.error', this.error) : null,
|
||||
m(Input, {
|
||||
label: lang.login_email, // Email or name
|
||||
form: this.form,
|
||||
formKey: 'email',
|
||||
}),
|
||||
m(Input, {
|
||||
label: lang.login_password, // Password
|
||||
type: 'password',
|
||||
form: this.form,
|
||||
formKey: 'password',
|
||||
}),
|
||||
m('input.spinner', {
|
||||
hidden: api.loading,
|
||||
type: 'submit',
|
||||
value: lang.login_submit, // Log in
|
||||
}),
|
||||
api.loading ? m('div.loading-spinner') : null,
|
||||
]),
|
||||
]),
|
||||
m('footer', lang.mformat(
|
||||
lang.unsplash, // Photo by X on Y
|
||||
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'),
|
||||
)),
|
||||
]),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = Login
|
|
@ -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
|
|
@ -0,0 +1,227 @@
|
|||
const m = require('mithril')
|
||||
const Authentication = require('./authentication')
|
||||
const api = require('./api')
|
||||
const Input = require('./input')
|
||||
const lang = require('./lang')
|
||||
const videos = require('./videos')
|
||||
|
||||
const Upload = {
|
||||
oninit: function(vnode) {
|
||||
Authentication.requiresLogin()
|
||||
this.error = ''
|
||||
let d = new Date()
|
||||
d.setDate(d.getDate() - d.getDay())
|
||||
d.setHours(11)
|
||||
d.setMinutes(0)
|
||||
d.setSeconds(0)
|
||||
d.setMilliseconds(0)
|
||||
|
||||
this.cacheVideo = null
|
||||
this.cacheImage = null
|
||||
this.uploading = null
|
||||
this.bannerStatus = null
|
||||
this.form = {
|
||||
title: 'Sunnudagssamkoma',
|
||||
date: d,
|
||||
file: null,
|
||||
banner: null,
|
||||
metadata: {
|
||||
speaker: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
uploadvideo: 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.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',
|
||||
})
|
||||
.then(res => {
|
||||
if (this.cacheImage?.file === this.form.banner) {
|
||||
return this.cacheImage
|
||||
}
|
||||
|
||||
return api.uploadBanner(this.form.banner, res, (status) => {
|
||||
this.bannerStatus = status
|
||||
m.redraw()
|
||||
})
|
||||
.then(imageData => {
|
||||
this.cacheImage = imageData
|
||||
return res
|
||||
})
|
||||
})
|
||||
.then(res => {
|
||||
if (this.cacheVideo?.file === this.form.file) {
|
||||
return this.cacheVideo
|
||||
}
|
||||
|
||||
return api.uploadFileProgress({
|
||||
url: res.path + '?token=' + res.token,
|
||||
}, this.form.file, (xhr, progress, perSecond) => {
|
||||
this.uploading = {
|
||||
progress,
|
||||
xhr,
|
||||
perSecond
|
||||
}
|
||||
m.redraw()
|
||||
})
|
||||
})
|
||||
.then(res => {
|
||||
this.cacheVideo = {
|
||||
file: this.form.file,
|
||||
filename: res.filename,
|
||||
path: res.path,
|
||||
}
|
||||
this.uploading = null
|
||||
|
||||
return api.sendRequest({
|
||||
method: 'PUT',
|
||||
url: '/api/auth/articles/0',
|
||||
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: {
|
||||
filename: res.filename,
|
||||
path: res.path,
|
||||
type: this.form.file.type,
|
||||
size: this.form.file.size,
|
||||
},
|
||||
banner: {
|
||||
filename: this.cacheImage.medium.filename,
|
||||
path: this.cacheImage.medium.path,
|
||||
type: 'image/avif',
|
||||
size: this.cacheImage.size,
|
||||
preview: {
|
||||
base64: this.cacheImage.preview.base64,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
.then(res => {
|
||||
videos.refreshTree()
|
||||
m.route.set('/')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.bannerStatus = null
|
||||
this.uploading = null
|
||||
if (!error) return
|
||||
|
||||
this.error = lang.format(lang.upload_error, error.message) // Error while uploading:
|
||||
})
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
cancelUpload(e) {
|
||||
e.stopPropagation()
|
||||
this.uploading.xhr.abortRequest()
|
||||
this.uploading = null
|
||||
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', [
|
||||
m('div.modal', [
|
||||
m('form', {
|
||||
onsubmit: this.uploadvideo.bind(this, vnode),
|
||||
}, [
|
||||
m('h3', 'Upload new video'),
|
||||
this.error ? m('p.error', this.error) : null,
|
||||
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(Input, {
|
||||
label: 'Video',
|
||||
type: 'file',
|
||||
accept: '.webm',
|
||||
utility: 'file',
|
||||
button: 'fa-video',
|
||||
form: this.form,
|
||||
formKey: 'file',
|
||||
oninput: (file) => this.filechanged(file),
|
||||
}),
|
||||
m(Input, {
|
||||
label: 'Mynd',
|
||||
type: 'file',
|
||||
accept: 'image/*',
|
||||
utility: 'image',
|
||||
form: this.form,
|
||||
formKey: 'banner',
|
||||
}),
|
||||
m('p.separator', 'Optional'),
|
||||
m(Input, {
|
||||
label: 'Speaker',
|
||||
form: this.form.metadata,
|
||||
formKey: 'speaker',
|
||||
}),
|
||||
m('input.spinner', {
|
||||
hidden: api.loading,
|
||||
type: 'submit',
|
||||
value: 'Begin upload',
|
||||
}),
|
||||
api.loading ? m('div.loading-spinner') : null,
|
||||
this.bannerStatus ? [
|
||||
m('p', this.bannerStatus),
|
||||
m('.loading-bar', { style: `--progress: 0%` }),
|
||||
] : null,
|
||||
this.uploading ? [
|
||||
m('p', `${Math.floor(this.uploading.progress)}% (${this.uploading.perSecond}/s)`),
|
||||
m('.loading-bar', { style: `--progress: ${this.uploading.progress}%` }),
|
||||
m('button.button.button-alert', {
|
||||
onclick: this.cancelUpload.bind(this),
|
||||
}, 'Cancel upload'),
|
||||
] : null,
|
||||
]),
|
||||
]),
|
||||
m('footer', lang.mformat(
|
||||
lang.unsplash, // Photo by X on Y
|
||||
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'),
|
||||
)),
|
||||
]),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = Upload
|
|
@ -0,0 +1,120 @@
|
|||
const m = require('mithril')
|
||||
const api = require('./api')
|
||||
|
||||
const Tree = []
|
||||
const Articles = []
|
||||
|
||||
exports.Tree = Tree
|
||||
exports.Articles = Articles
|
||||
|
||||
exports.loading = false
|
||||
exports.error = ''
|
||||
exports.year = null
|
||||
exports.month = null
|
||||
|
||||
const matcher = /\/(\d+)(\/\d+)?/
|
||||
|
||||
function calculateActiveBranches() {
|
||||
let path = matcher.exec(m.route.get())
|
||||
if (path && path[1] !== exports.year?.title) {
|
||||
for (let year of Tree) {
|
||||
if (year.title === path[1]) {
|
||||
exports.year = year
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (!path && m.route.get() === '/') {
|
||||
exports.year = Tree[Tree.length - 1]
|
||||
} else if (!path) {
|
||||
exports.year = null
|
||||
}
|
||||
if (path && exports.year && path[2]) {
|
||||
exports.month = exports.year.branches[Number(path[2].slice(1)) - 1] || null
|
||||
} else if (!path?.[2]) {
|
||||
exports.month = null
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildTree() {
|
||||
Tree.splice(0, Tree.length)
|
||||
|
||||
if (!Articles.length) return
|
||||
|
||||
let startYear = Articles[0].publish_at
|
||||
let target = new Date()
|
||||
let articleIndex = 0
|
||||
|
||||
for (let year = startYear.getFullYear(); year <= target.getFullYear(); year++) {
|
||||
let branchYear = {
|
||||
title: year.toString(),
|
||||
type: 'year',
|
||||
branches: [],
|
||||
videos: []
|
||||
}
|
||||
Tree.push(branchYear)
|
||||
let lastMonth = year === target.getFullYear() ? target.getMonth() + 1 : 12
|
||||
|
||||
for (let month = 1; month <= lastMonth; month++) {
|
||||
let branchMonth = {
|
||||
title: month.toString(),
|
||||
type: 'month',
|
||||
branches: [],
|
||||
videos: []
|
||||
}
|
||||
branchYear.branches.push(branchMonth)
|
||||
|
||||
let start = new Date(year, month - 1)
|
||||
let end = new Date(year, month)
|
||||
|
||||
for (; Articles[articleIndex] && Articles[articleIndex].publish_at >= start && Articles[articleIndex].publish_at < end; articleIndex++) {
|
||||
branchYear.videos.push(Articles[articleIndex])
|
||||
branchMonth.videos.push(Articles[articleIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeArticle(id) {
|
||||
let index = Articles.findIndex(article => article.id === id)
|
||||
if (index >= 0) {
|
||||
Articles.splice(index, 1)
|
||||
rebuildTree()
|
||||
}
|
||||
}
|
||||
|
||||
function refreshTree() {
|
||||
exports.error = ''
|
||||
|
||||
if (exports.loading) return Promise.resolve()
|
||||
|
||||
exports.loading = true
|
||||
|
||||
m.redraw()
|
||||
|
||||
return api.sendRequest({
|
||||
method: 'GET',
|
||||
url: '/api/articles',
|
||||
})
|
||||
.then(result => {
|
||||
result.videos.forEach(video => {
|
||||
video.publish_at = new Date(video.publish_at)
|
||||
video.path_short = video.path.split('-')[2]
|
||||
})
|
||||
|
||||
Articles.splice(0, Articles.length)
|
||||
Articles.push.apply(Articles, result.videos)
|
||||
|
||||
rebuildTree()
|
||||
}, err => {
|
||||
exports.error = 'Error fetching videos: ' + err.message
|
||||
})
|
||||
.then(() => {
|
||||
exports.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
}
|
||||
|
||||
exports.removeArticle = removeArticle
|
||||
exports.rebuildTree = rebuildTree
|
||||
exports.refreshTree = refreshTree
|
||||
exports.calculateActiveBranches = calculateActiveBranches
|
|
@ -0,0 +1 @@
|
|||
../base
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"scripts": {
|
||||
"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",
|
||||
"mithril": "^2.2.2",
|
||||
"service-core": "^3.0.0-beta.17"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import fs from 'fs'
|
||||
import { pathToFileURL } from 'url'
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
import('service-core').then(core => {
|
||||
const port = 4130
|
||||
|
||||
var core = new core.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({ start }).then(function() {
|
||||
return core.run()
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"name": "filadelfia_archive",
|
||||
"version": "1.0.8",
|
||||
"port": 4130,
|
||||
"description": "Filadelfia archive",
|
||||
"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": "esbuild app/index.js --bundle --outfile=public/assets/app.js",
|
||||
"dev:build": "eltro --watch build --npm build",
|
||||
"dev:server": "eltro --watch server --npm server",
|
||||
"dev:build:old": "npm-watch build",
|
||||
"dev:server:old": "npm-watch server",
|
||||
"server": "node index.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",
|
||||
"flaska": "^1.3.2",
|
||||
"formidable": "^1.2.6",
|
||||
"msnodesqlv8": "^4.1.1",
|
||||
"nconf-lite": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eonasdan/tempus-dominus": "^6.7.19",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"eltro": "^1.4.4",
|
||||
"esbuild": "^0.19.5",
|
||||
"mithril": "^2.2.2",
|
||||
"service-core": "^3.0.0-beta.17"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 3.1 KiB |
|
@ -0,0 +1,101 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="26.656599mm"
|
||||
height="26.65659mm"
|
||||
viewBox="0 0 26.656599 26.65659"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3.1 (91b66b0783, 2023-11-16)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showguides="true"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.2083894"
|
||||
inkscape:cx="-36.712382"
|
||||
inkscape:cy="66.890197"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1377"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1">
|
||||
<sodipodi:guide
|
||||
position="21.496619,297"
|
||||
orientation="0,-1"
|
||||
id="guide1"
|
||||
inkscape:locked="false" />
|
||||
<inkscape:grid
|
||||
id="grid2"
|
||||
units="mm"
|
||||
originx="-90.44421"
|
||||
originy="-127.62701"
|
||||
spacingx="0.99999998"
|
||||
spacingy="1"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
dotted="false"
|
||||
gridanglex="30"
|
||||
gridanglez="30"
|
||||
visible="false" />
|
||||
<sodipodi:guide
|
||||
position="-2.3731349e-07,282.22545"
|
||||
orientation="1,0"
|
||||
id="guide2"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="26.656599,279.39628"
|
||||
orientation="1,0"
|
||||
id="guide3"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="8.8910958,270.34341"
|
||||
orientation="0,-1"
|
||||
id="guide4"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-90.444211,-127.62701)">
|
||||
<path
|
||||
id="path1"
|
||||
d="M 90.444211,154.2836 H 117.10081 V 127.62701 H 90.444211 Z"
|
||||
style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.159661" />
|
||||
<path
|
||||
id="path224"
|
||||
d="m 103.77216,127.62701 c 7.36141,0 13.32865,5.96724 13.32865,13.3283 0,7.36106 -5.96724,13.32829 -13.32865,13.32829 -7.360713,0 -13.327947,-5.96723 -13.327947,-13.32829 0,-7.36106 5.967234,-13.3283 13.327947,-13.3283"
|
||||
style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.352778" />
|
||||
<path
|
||||
id="path225"
|
||||
d="m 103.77216,128.3629 c -6.954663,0 -12.592405,5.63774 -12.592405,12.5924 0,6.95466 5.637742,12.5924 12.592405,12.5924 6.95501,0 12.59275,-5.63774 12.59275,-12.5924 0,-6.95466 -5.63774,-12.5924 -12.59275,-12.5924"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.352778" />
|
||||
<path
|
||||
id="path226"
|
||||
d="m 103.74393,129.68123 c 6.19478,0 11.21586,5.02179 11.21586,11.21622 0,6.19477 -5.02108,11.21621 -11.21586,11.21621 -6.19442,0 -11.216219,-5.02144 -11.216219,-11.21621 0,-6.19443 5.021799,-11.21622 11.216219,-11.21622"
|
||||
style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.352778" />
|
||||
<path
|
||||
id="path227"
|
||||
d="m 102.98229,131.02708 v 5.04931 h 3.34469 c 0.56162,-1.17581 2.25918,-2.76684 3.26989,-3.64631 l -0.45332,1.06045 c 0.0794,-0.067 -0.70238,1.29046 0.009,2.81799 0.63217,1.35713 1.07032,2.09867 1.2894,3.73097 0.35278,-0.94015 0.56691,-1.62736 1.04034,-2.17628 0.27552,-0.31856 0.31538,-0.93698 0.27023,-1.63971 0.38488,0.42192 0.93556,2.09126 0.82726,3.4364 -0.0536,0.67134 0.10936,2.83175 -2.17523,3.46358 -0.36794,1.33879 -1.24283,2.57704 -2.68993,3.37961 -1.78646,0.9906 -2.58057,0.20249 -3.81987,2.15512 -0.0596,-0.33691 -0.0677,-0.71932 -0.0773,-0.99554 -0.12065,-0.1584 -0.20885,-0.26988 -0.26494,-0.57397 -0.0603,-0.16052 0.001,-0.51082 0.26423,-0.57256 -0.0127,-1.36984 1.36384,-1.15641 1.60585,-1.95474 0.52422,-1.24848 -1.57445,-1.39877 -2.44052,-2.12513 v 6.70877 c 0.30127,0.17286 0.58526,0.44062 0.84772,0.85478 1.29082,-2.03412 3.09175,-0.54151 4.95265,-1.57339 l 1.83021,0.63077 -0.1337,0.1076 c -2.52412,1.23119 -4.95406,-0.67381 -6.70772,2.0902 -1.78223,-2.80846 -4.264368,-0.79586 -6.832603,-2.15229 l 1.937463,-0.67628 c 0.670632,0.37183 1.33315,0.41593 1.96744,0.41522 v -10.6306 h -5.036613 c -0.20179,0.0473 -0.21449,-2.18899 0,-2.13713 l 5.036613,0.002 -0.001,-5.04931 c -0.0483,-0.21449 2.18969,-0.19614 2.1396,0 z m 3.31576,7.18397 h -1.01177 c 0.004,0.32773 10e-4,0.62194 10e-4,0.85901 0,0.78317 1.59526,1.75789 2.0387,1.95827 0.14958,-1.2125 -0.12029,-1.60549 -0.8128,-2.50049 -0.0832,-0.10689 -0.15451,-0.21167 -0.21519,-0.31679"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.352778" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.0 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="100.749" height="100.749" viewBox="0 0 26.657 26.657"><path d="M103.772 127.627c7.362 0 13.329 5.967 13.329 13.328s-5.967 13.329-13.329 13.329c-7.36 0-13.328-5.968-13.328-13.329 0-7.36 5.967-13.328 13.328-13.328" style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.352778" transform="translate(-90.444 -127.627)"/><path d="M103.772 128.363c-6.955 0-12.592 5.638-12.592 12.592 0 6.955 5.637 12.593 12.592 12.593 6.955 0 12.593-5.638 12.593-12.593 0-6.954-5.638-12.592-12.593-12.592" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.352778" transform="translate(-90.444 -127.627)"/><path d="M103.744 129.681c6.195 0 11.216 5.022 11.216 11.216 0 6.195-5.021 11.217-11.216 11.217-6.194 0-11.216-5.022-11.216-11.217 0-6.194 5.022-11.216 11.216-11.216" style="fill:#18597d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.352778" transform="translate(-90.444 -127.627)"/><path d="M102.982 131.027v5.05h3.345c.562-1.176 2.26-2.767 3.27-3.647l-.453 1.06c.079-.066-.703 1.291.009 2.819.632 1.357 1.07 2.098 1.289 3.73.353-.94.567-1.627 1.04-2.176.276-.318.316-.937.27-1.64.385.422.936 2.092.828 3.437-.054.671.11 2.832-2.175 3.463-.368 1.34-1.243 2.578-2.69 3.38-1.787.99-2.581.203-3.82 2.155-.06-.337-.068-.72-.078-.995-.12-.159-.208-.27-.264-.574-.06-.16 0-.511.264-.573-.013-1.37 1.364-1.156 1.606-1.955.524-1.248-1.575-1.398-2.44-2.125v6.709c.3.173.584.44.847.855 1.29-2.034 3.092-.542 4.952-1.574l1.83.631-.133.108c-2.524 1.231-4.954-.674-6.708 2.09-1.782-2.808-4.264-.796-6.832-2.152l1.937-.677c.67.372 1.333.416 1.968.416v-10.63h-5.037c-.202.046-.215-2.19 0-2.138l5.037.002-.001-5.05c-.049-.214 2.19-.196 2.14 0zm3.316 7.184h-1.012c.004.328.001.622.001.86 0 .782 1.596 1.757 2.039 1.957.15-1.212-.12-1.605-.813-2.5a2.774 2.774 0 0 1-.215-.317" style="fill:#fff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.352778" transform="translate(-90.444 -127.627)"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,700 @@
|
|||
.visually-hidden, .tempus-dominus-widget [data-action]::after {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.tempus-dominus-widget {
|
||||
list-style: none;
|
||||
padding: 4px;
|
||||
width: 19rem;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.tempus-dominus-widget.calendarWeeks {
|
||||
width: 21rem;
|
||||
}
|
||||
.tempus-dominus-widget.calendarWeeks .date-container-days {
|
||||
grid-auto-columns: 12.5%;
|
||||
grid-template-areas: "a a a a a a a a";
|
||||
}
|
||||
.tempus-dominus-widget [data-action] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.tempus-dominus-widget [data-action]::after {
|
||||
content: attr(title);
|
||||
}
|
||||
.tempus-dominus-widget [data-action].disabled, .tempus-dominus-widget [data-action].disabled:hover {
|
||||
background: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.tempus-dominus-widget .arrow {
|
||||
display: none;
|
||||
}
|
||||
.tempus-dominus-widget.show {
|
||||
display: block;
|
||||
}
|
||||
.tempus-dominus-widget.show.date-container {
|
||||
min-height: 315px;
|
||||
}
|
||||
.tempus-dominus-widget.show.time-container {
|
||||
min-height: 217px;
|
||||
}
|
||||
.tempus-dominus-widget .td-collapse:not(.show) {
|
||||
display: none;
|
||||
}
|
||||
.tempus-dominus-widget .td-collapsing {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: height 0.35s ease;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.tempus-dominus-widget.timepicker-sbs {
|
||||
width: 38em;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.tempus-dominus-widget.timepicker-sbs {
|
||||
width: 38em;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.tempus-dominus-widget.timepicker-sbs {
|
||||
width: 38em;
|
||||
}
|
||||
}
|
||||
.tempus-dominus-widget.timepicker-sbs .td-row {
|
||||
display: flex;
|
||||
}
|
||||
.tempus-dominus-widget.timepicker-sbs .td-row .td-half {
|
||||
flex: 0 0 auto;
|
||||
width: 50%;
|
||||
}
|
||||
.tempus-dominus-widget div[data-action]:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
.tempus-dominus-widget .timepicker-hour,
|
||||
.tempus-dominus-widget .timepicker-minute,
|
||||
.tempus-dominus-widget .timepicker-second {
|
||||
width: 54px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
margin: 0;
|
||||
}
|
||||
.tempus-dominus-widget button[data-action] {
|
||||
padding: 6px;
|
||||
}
|
||||
.tempus-dominus-widget .toggleMeridiem {
|
||||
text-align: center;
|
||||
height: 38px;
|
||||
}
|
||||
.tempus-dominus-widget .calendar-header {
|
||||
display: grid;
|
||||
grid-template-areas: "a a a";
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tempus-dominus-widget .calendar-header .next {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.tempus-dominus-widget .calendar-header .previous {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.tempus-dominus-widget .calendar-header .picker-switch {
|
||||
text-align: center;
|
||||
}
|
||||
.tempus-dominus-widget .toolbar {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-rows: 40px;
|
||||
}
|
||||
.tempus-dominus-widget .toolbar div {
|
||||
border-radius: 999px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-days {
|
||||
display: grid;
|
||||
grid-template-areas: "a a a a a a a";
|
||||
grid-auto-rows: 40px;
|
||||
grid-auto-columns: 14.2857142857%;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-days .range-in {
|
||||
background-color: #01419e !important;
|
||||
border: none;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: -5px 0 0 #01419e, 5px 0 0 #01419e;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-days .range-end {
|
||||
border-radius: 0 50px 50px 0 !important;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-days .range-start {
|
||||
border-radius: 50px 0 0 50px !important;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-days .dow {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-days .cw {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
font-size: 0.8em;
|
||||
line-height: 20px;
|
||||
cursor: default;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-decades,
|
||||
.tempus-dominus-widget .date-container-years,
|
||||
.tempus-dominus-widget .date-container-months {
|
||||
display: grid;
|
||||
grid-template-areas: "a a a";
|
||||
grid-auto-rows: calc((19rem - 8px) / 7);
|
||||
}
|
||||
.tempus-dominus-widget .time-container-hour,
|
||||
.tempus-dominus-widget .time-container-minute,
|
||||
.tempus-dominus-widget .time-container-second {
|
||||
display: grid;
|
||||
grid-template-areas: "a a a a";
|
||||
grid-auto-rows: calc((19rem - 8px) / 7);
|
||||
}
|
||||
.tempus-dominus-widget .time-container-clock {
|
||||
display: grid;
|
||||
grid-auto-rows: calc((19rem - 8px) / 7);
|
||||
}
|
||||
.tempus-dominus-widget .time-container-clock .no-highlight {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-decades div:not(.no-highlight),
|
||||
.tempus-dominus-widget .date-container-years div:not(.no-highlight),
|
||||
.tempus-dominus-widget .date-container-months div:not(.no-highlight),
|
||||
.tempus-dominus-widget .date-container-days div:not(.no-highlight),
|
||||
.tempus-dominus-widget .time-container-clock div:not(.no-highlight),
|
||||
.tempus-dominus-widget .time-container-hour div:not(.no-highlight),
|
||||
.tempus-dominus-widget .time-container-minute div:not(.no-highlight),
|
||||
.tempus-dominus-widget .time-container-second div:not(.no-highlight) {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
border-radius: 999px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-decades div:not(.no-highlight).disabled, .tempus-dominus-widget .date-container-decades div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget .date-container-years div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget .date-container-years div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget .date-container-months div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget .date-container-months div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget .date-container-days div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget .date-container-days div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget .time-container-clock div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget .time-container-clock div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget .time-container-hour div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget .time-container-hour div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget .time-container-minute div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget .time-container-minute div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget .time-container-second div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget .time-container-second div:not(.no-highlight).disabled:hover {
|
||||
background: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-decades div:not(.no-highlight).today,
|
||||
.tempus-dominus-widget .date-container-years div:not(.no-highlight).today,
|
||||
.tempus-dominus-widget .date-container-months div:not(.no-highlight).today,
|
||||
.tempus-dominus-widget .date-container-days div:not(.no-highlight).today,
|
||||
.tempus-dominus-widget .time-container-clock div:not(.no-highlight).today,
|
||||
.tempus-dominus-widget .time-container-hour div:not(.no-highlight).today,
|
||||
.tempus-dominus-widget .time-container-minute div:not(.no-highlight).today,
|
||||
.tempus-dominus-widget .time-container-second div:not(.no-highlight).today {
|
||||
position: relative;
|
||||
}
|
||||
.tempus-dominus-widget .date-container-decades div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget .date-container-years div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget .date-container-months div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget .date-container-days div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget .time-container-clock div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget .time-container-hour div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget .time-container-minute div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget .time-container-second div:not(.no-highlight).today:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border: solid transparent;
|
||||
border-width: 0 0 7px 7px;
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
.tempus-dominus-widget .time-container {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.tempus-dominus-widget button {
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.day,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.hour,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.minute,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.second,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementHours],
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementMinutes],
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementSeconds],
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementHours],
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementMinutes],
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementSeconds],
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showHours],
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showMinutes],
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showSeconds],
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=togglePeriod] {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.day:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.hour:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.minute:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td.second:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementHours]:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementMinutes]:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=incrementSeconds]:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementHours]:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementMinutes]:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=decrementSeconds]:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showHours]:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showMinutes]:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=showSeconds]:hover,
|
||||
.tempus-dominus-widget.tempus-dominus-widget-readonly table td [data-action=togglePeriod]:hover {
|
||||
background: none;
|
||||
}
|
||||
.tempus-dominus-widget.light {
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
.tempus-dominus-widget.light [data-action].disabled, .tempus-dominus-widget.light [data-action].disabled:hover {
|
||||
color: #6c757d;
|
||||
}
|
||||
.tempus-dominus-widget.light .toolbar div:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
.tempus-dominus-widget.light .date-container-days .dow {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.tempus-dominus-widget.light .date-container-days .cw {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight):hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight),
|
||||
.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight),
|
||||
.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight),
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active {
|
||||
background-color: #0d6efd;
|
||||
color: #fff;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.old, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-in:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-in:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-end:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-end:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-start:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-start:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.new, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-in:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-in:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-end:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-end:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-decades .date-container-days div.range-start:not(.no-highlight).new, .tempus-dominus-widget.light .date-container-days .date-container-decades div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-years .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-years div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-months .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .date-container-months div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-clock .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-clock div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-hour .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-hour div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-minute .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-minute div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-second .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days .time-container-second div.range-start:not(.no-highlight).new {
|
||||
color: #fff;
|
||||
}
|
||||
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-in:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-end:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .date-container-days div.range-start:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).active.today:before {
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).old, .tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).new {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled, .tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).disabled:hover {
|
||||
color: #6c757d;
|
||||
}
|
||||
.tempus-dominus-widget.light .date-container-decades div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .date-container-years div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .date-container-months div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .date-container-days div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .time-container-clock div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .time-container-hour div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .time-container-minute div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.light .time-container-second div:not(.no-highlight).today:before {
|
||||
border-bottom-color: #0d6efd;
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.tempus-dominus-widget.light button {
|
||||
color: #fff;
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
.tempus-dominus-widget.dark {
|
||||
color: #e3e3e3;
|
||||
background-color: #1b1b1b;
|
||||
}
|
||||
.tempus-dominus-widget.dark [data-action].disabled, .tempus-dominus-widget.dark [data-action].disabled:hover {
|
||||
color: #6c757d;
|
||||
}
|
||||
.tempus-dominus-widget.dark .toolbar div:hover {
|
||||
background: rgb(35, 38, 39);
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-days .dow {
|
||||
color: rgba(232, 230, 227, 0.5);
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-days .range-in {
|
||||
background-color: #0071c7 !important;
|
||||
box-shadow: -5px 0 0 #0071c7, 5px 0 0 #0071c7;
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-days .cw {
|
||||
color: rgba(232, 230, 227, 0.38);
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight):hover,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight):hover {
|
||||
background: rgb(35, 38, 39);
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight),
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight),
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight),
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active {
|
||||
background-color: #4db2ff;
|
||||
color: #fff;
|
||||
text-shadow: 0 -1px 0 rgba(232, 230, 227, 0.25);
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.old, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-in:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-in:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-end:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-end:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-start:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-start:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.new, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-in:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-in:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-end:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-end:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-decades .date-container-days div.range-start:not(.no-highlight).new, .tempus-dominus-widget.dark .date-container-days .date-container-decades div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-years .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-years div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-months .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .date-container-months div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-clock .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-clock div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-hour .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-hour div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-minute .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-minute div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.old,
|
||||
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-in:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-end:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-start:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.new,
|
||||
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-in:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-end:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-second .date-container-days div.range-start:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days .time-container-second div.range-start:not(.no-highlight).new {
|
||||
color: #fff;
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-in:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-end:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .date-container-days div.range-start:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).active.today:before,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).active.today:before {
|
||||
border-bottom-color: #1b1b1b;
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).old, .tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).new,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).old,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).new {
|
||||
color: rgba(232, 230, 227, 0.38);
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled, .tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).disabled:hover,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).disabled:hover {
|
||||
color: #6c757d;
|
||||
}
|
||||
.tempus-dominus-widget.dark .date-container-decades div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .date-container-years div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .date-container-months div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .date-container-days div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .time-container-clock div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .time-container-hour div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .time-container-minute div:not(.no-highlight).today:before,
|
||||
.tempus-dominus-widget.dark .time-container-second div:not(.no-highlight).today:before {
|
||||
border-bottom-color: #4db2ff;
|
||||
border-top-color: rgba(232, 230, 227, 0.2);
|
||||
}
|
||||
.tempus-dominus-widget.dark button {
|
||||
color: #fff;
|
||||
background-color: #4db2ff;
|
||||
border-color: #4db2ff;
|
||||
}
|
||||
/*# sourceMappingURL=tempus-dominus.css.map */
|
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,213 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Filadelfia myndhvelfing</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png">
|
||||
<style>
|
||||
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
:root { --bg: #fff; --bg-component: #f3f7ff; --bg-component-half: #f3f7ff77; --bg-component-alt: #ffd99c; --color: #031131; --color-alt: #7a9ad3; --main: #18597d; --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; }
|
||||
h1 { margin-bottom: 1rem; }
|
||||
#main { flex: 2 1 auto; display: flex; flex-direction: column; }
|
||||
.page { 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; }
|
||||
.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; margin-top: 0.5rem; 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); } }
|
||||
|
||||
/* Common components */
|
||||
|
||||
.row { display: flex; }
|
||||
.column { display: flex; flex-direction: column; }
|
||||
.filler { flex-grow: 2; }
|
||||
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=datetime] { border: 1px solid var(--main); background: #fff; color: var(--color); border-radius: 0; padding: 0.25rem; line-height: 1rem; outline: none; width: 100%; }
|
||||
|
||||
input[type=text]:disabled,
|
||||
input[type=password]:disabled,
|
||||
input[type=datetime]:disabled { background: var(--bg-component); border-color: var(--color-alt); color: var(--color-alt); }
|
||||
|
||||
.form-row input:disabled + button { border-color: var(--color-alt); }
|
||||
.form-row { display: flex; position: relative; flex-wrap: wrap; }
|
||||
.form-row input { flex: 2 1 auto; }
|
||||
.form-row > input { width: auto; }
|
||||
.form-row .form-column { display: flex; flex-direction: column; justify-content: space-around; }
|
||||
.form-row button { min-width: 30px; text-align: center; border: 1px solid var(--main); border-left: none; background: var(--bg-component); text-decoration: none; }
|
||||
.form-row.image-banner { background-size: cover; background-color: white; border: 2px dashed var(--main); aspect-ratio: 16 / 9; align-self: center; width: 100%; max-width: 360px; }
|
||||
.form-row .cover { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
|
||||
|
||||
input[type=text]:focus,
|
||||
input[type=password]:focus,
|
||||
input[type=datetime]:focus { outline: 1px solid var(--main); }
|
||||
.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; text-decoration: none; }
|
||||
|
||||
.button.spinner, input[type=submit].spinner { height: 2rem; margin-top: 2rem; margin-bottom: 1.5rem; }
|
||||
.button-alert { background: var(--error-bg); color: var(--color); }
|
||||
|
||||
.loading-bar { border: 1px solid var(--main); background: var(--bg-component); }
|
||||
.loading-bar::after { height: 1rem; background: var(--main); min-width: 1px; content: ''; display: block; width: var(--progress); transition: width 3s; }
|
||||
|
||||
form p, label { font-size: 0.75rem; font-weight: 500; margin: 0.75rem 0 0.5rem 0; display: block; }
|
||||
form p.separator { color: var(--color-alt); margin-top: 1.5rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-alt); }
|
||||
|
||||
/* Nav */
|
||||
|
||||
#header { background: var(--bg-component); }
|
||||
|
||||
#header nav { display: flex; }
|
||||
#header nav,
|
||||
#header .nav { text-align: center; padding: 0.5rem 1rem 0.5rem 0; justify-content: flex-end; flex-wrap: wrap; }
|
||||
|
||||
#header nav a,
|
||||
#header nav button { margin-left: 1rem; }
|
||||
|
||||
#header h4 { flex: 2 1 auto; margin: -0.5rem; }
|
||||
|
||||
#header h4 a,
|
||||
#header h4 a:visited { background: url('/assets/logo_nobg.svg') left center no-repeat; background-size: auto calc(3rem - 4px); height: 3rem; display: inline-block; padding-left: 3rem; line-height: 3rem; }
|
||||
|
||||
#header .link { font-size: 1.0rem; line-height: 2rem; font-weight: normal; padding: 0 0.5rem; }
|
||||
#header .changelang { font-size: 1.2rem; }
|
||||
#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); }
|
||||
#header .nav { overflow-x: hidden; padding: 0.75rem 1rem 0.25rem; min-height: 4rem; align-items: flex-start; border-bottom: 1px solid #0001; }
|
||||
#header .nav:hover { overflow-x: auto; }
|
||||
#header .nav .inner { padding-top: 0.5rem; }
|
||||
#header .nav a { margin: 0 0.25rem; padding: 0.25rem 1.5rem; border-radius: 3rem; }
|
||||
#header .nav a.empty { opacity: 0.5; }
|
||||
#header .nav a.active { background: var(--bg-component-alt); color: var(--color); text-decoration: none; }
|
||||
#header .error { background: var(--error-bg); color: var(--color); font-size: 0.8rem; text-align: center; padding: 0.25rem; cursor: pointer; }
|
||||
|
||||
/* 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; }
|
||||
|
||||
footer { text-align: center; padding: 1rem; }
|
||||
|
||||
footer a { font-size: 0.8rem; }
|
||||
|
||||
/* login */
|
||||
/* upload */
|
||||
|
||||
.page-login,
|
||||
.page-upload { background-image: url('./assets/bg.avif'); background-repeat: no-repeat; background-position: center; background-size: cover; }
|
||||
.page-login .modal form,
|
||||
.page-upload .modal form { backdrop-filter: blur(10px); background: var(--bg-component-half); }
|
||||
|
||||
/* browse */
|
||||
|
||||
.gallery { margin: 1rem; display: flex; flex-direction: column; }
|
||||
.gallery-year { margin: 1rem; padding: 0 0 1rem; border-bottom: 1px solid var(--main); text-align: center; font-size: 2rem; }
|
||||
.gallery-month { margin: 0rem 1rem 1rem; font-size: 1.2rem; border-bottom: 1px solid #0003; }
|
||||
.gallery .group { display: flex; flex-wrap: wrap; }
|
||||
.gallery .group a { width: calc(50vw - 4rem); max-width: 320px; aspect-ratio: 16 / 9; display: flex; flex-direction: column; justify-content: flex-end; background: url('./assets/placeholder.avif') center no-repeat; background-size: cover; margin: 0 1rem 1rem; text-align: center; border: 1px solid var(--main); }
|
||||
|
||||
.gallery .group a span { align-self: stretch; text-align: center; background: #fffb; }
|
||||
|
||||
/* Player */
|
||||
|
||||
.player { background: black; text-align: center; margin-bottom: 1rem; }
|
||||
.player video { margin: 0 auto; width: 1280px; max-width: 100%; aspect-ratio: 16 / 9; }
|
||||
|
||||
/* article */
|
||||
|
||||
.article { width: 100%; max-width: 1280px; padding: 0.5rem; align-self: center; margin-bottom: 5rem; }
|
||||
.article .full-error { margin-top: 1rem; }
|
||||
.article-name { flex: 2 1 auto; margin-left: 1rem; }
|
||||
.article h1 { margin: 0; }
|
||||
.article h1,
|
||||
.article p { padding: 0 0.5rem 0.5rem; }
|
||||
|
||||
/* table */
|
||||
|
||||
.table { display: grid; column-gap: 0; row-gap: 0; grid-template-columns: minmax(150px, 1.33fr) minmax(150px, 2.33fr); }
|
||||
.table-row { display: contents; }
|
||||
.table-row:nth-child(odd) .table-item { background: #f8f6ff; }
|
||||
.table-item { padding: 0.5rem; }
|
||||
|
||||
/* holdbutton */
|
||||
.holdbutton {
|
||||
--hold-bg: var(--bg-component);
|
||||
--hold-color: var(--main);
|
||||
--hold-fill: var(--main);
|
||||
--hold-fill-fg: white;
|
||||
display: inline-block;
|
||||
background: var(--hold-bg);
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
.holdbutton .inner {
|
||||
padding: 0.25rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background: var(--hold-color);
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.holdbutton.holdbutton-active {
|
||||
background: linear-gradient( var(--hold-fill) , var(--hold-fill)) var(--hold-bg) no-repeat 0 0;
|
||||
background-size: 0 100%;
|
||||
animation: stripes 2s linear 1 forwards;
|
||||
}
|
||||
.holdbutton.holdbutton-active div.inner {
|
||||
background: linear-gradient( var(--hold-fill-fg), var(--hold-fill-fg)) var(--hold-color) no-repeat 0 0;
|
||||
background-size: 0 100%;
|
||||
animation: stripes 2s linear 1 forwards;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
@keyframes stripes { to { background-size: 100% 100%; } }
|
||||
|
||||
@media (pointer:coarse) {
|
||||
#header .nav { overflow-x: scroll; }
|
||||
}
|
||||
@media screen and (max-width: 700px){
|
||||
.gallery, .gallery-year { margin: 0.25rem; }
|
||||
.gallery-month { margin: 0rem 0.25rem 0.25rem; }
|
||||
.gallery .group a { margin: 0 0.25rem 0.25rem; width: calc(50vw - 1rem); font-size: min(3vw, 1rem); }
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header"></div>
|
||||
<main id="main"></main>
|
||||
<script type="text/javascript" src="/assets/app.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 435 KiB |
|
@ -0,0 +1,144 @@
|
|||
const m = require('mithril')
|
||||
const util = require('./util')
|
||||
|
||||
let activeBox = null
|
||||
let boxIndex = 1
|
||||
|
||||
document.body.addEventListener('click', function() {
|
||||
activeBox = null
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
const Combobox = {
|
||||
oninit: function(vnode) {
|
||||
this.filtered = []
|
||||
this.open = false
|
||||
this.input = null
|
||||
this.id = boxIndex++
|
||||
this.onbeforeupdate(vnode)
|
||||
this.focus = this.onFocus.bind(this, vnode)
|
||||
},
|
||||
|
||||
onbeforeupdate: function(vnode) {
|
||||
if (!vnode.attrs.value) {
|
||||
this.filtered = vnode.attrs.items || []
|
||||
return
|
||||
}
|
||||
|
||||
let val = vnode.attrs.value.toLocaleLowerCase()
|
||||
this.filtered = vnode.attrs.items.filter(item => {
|
||||
return item.toLocaleLowerCase().indexOf(val) >= 0
|
||||
})
|
||||
},
|
||||
|
||||
onInput: function(vnode, e) {
|
||||
this.smartOpen(vnode)
|
||||
if (vnode.attrs.oninput) {
|
||||
vnode.attrs.oninput(e)
|
||||
}
|
||||
},
|
||||
|
||||
onDone: function(vnode) {
|
||||
if (vnode.attrs.ondone) {
|
||||
vnode.attrs.ondone()
|
||||
}
|
||||
},
|
||||
|
||||
onFocus: function(vnode) {
|
||||
this.input.focus()
|
||||
},
|
||||
|
||||
selectText: function(vnode, text) {
|
||||
this.input.value = text
|
||||
this.onInput(vnode, { target: this.input })
|
||||
activeBox = null
|
||||
this.onDone(vnode)
|
||||
return false
|
||||
},
|
||||
|
||||
onKeyPress: function(vnode, e) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
if (e.target.dataset.type === 'input' && e.target.nextElementSibling && e.target.nextElementSibling.childNodes) {
|
||||
e.target.nextElementSibling.childNodes[0].focus()
|
||||
} else if (e.target.dataset.type === 'item') {
|
||||
if (e.target.nextElementSibling) {
|
||||
e.target.nextElementSibling.focus()
|
||||
} else {
|
||||
this.input.focus()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
if (e.target.dataset.type === 'input' && e.target.nextElementSibling && e.target.nextElementSibling.lastChild) {
|
||||
e.target.nextElementSibling.scrollTop = e.target.nextElementSibling.scrollHeight
|
||||
e.target.nextElementSibling.lastChild.focus()
|
||||
} else if (e.target.dataset.type === 'item') {
|
||||
if (e.target.previousElementSibling) {
|
||||
e.target.previousElementSibling.focus()
|
||||
} else {
|
||||
this.input.focus()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
let newVal = ''
|
||||
if (e.target.dataset.type === 'input' && this.filtered.length) {
|
||||
if (e.target.value && e.target.value !== this.filtered[0]) {
|
||||
return this.selectText(vnode, this.filtered[0])
|
||||
}
|
||||
this.onDone(vnode)
|
||||
return false
|
||||
}
|
||||
if (e.target.dataset.type === 'item') {
|
||||
return this.selectText(vnode, e.target.dataset.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
smartOpen: function(vnode) {
|
||||
if (this.input.value && this.input.value === this.filtered[0]) {
|
||||
activeBox = null
|
||||
} else {
|
||||
activeBox = this.id
|
||||
}
|
||||
m.redraw()
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
closeBox = false
|
||||
|
||||
return m('div.form-item.combobox', {
|
||||
class: vnode.attrs.class,
|
||||
onclick: util.cancelPropagation,
|
||||
}, [
|
||||
m('label', vnode.attrs.label),
|
||||
m('input', {
|
||||
'data-type': 'input',
|
||||
oncreate: (e) => { this.input = e.dom },
|
||||
onkeydown: (e) => this.onKeyPress(vnode, e),
|
||||
type: vnode.attrs.type || 'text',
|
||||
value: vnode.attrs.value,
|
||||
onfocus: () => this.smartOpen(vnode),
|
||||
placeholder: vnode.attrs.placeholder || '',
|
||||
oninput: this.onInput.bind(this, vnode),
|
||||
}),
|
||||
activeBox === this.id
|
||||
? m('div.combobox-list', [
|
||||
this.filtered.slice(0, 50).map(item => {
|
||||
return m('div.combobox-list-item', {
|
||||
'data-type': 'item',
|
||||
'data-value': item,
|
||||
onkeydown: (e) => this.onKeyPress(vnode, e),
|
||||
onclick: (e) => this.selectText(vnode, item),
|
||||
tabindex: '0',
|
||||
}, item)
|
||||
})
|
||||
])
|
||||
: null,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Combobox
|
|
@ -1,15 +1,18 @@
|
|||
const m = require('mithril')
|
||||
|
||||
const Combobox = require('./combobox')
|
||||
const Constants = require('./consts')
|
||||
|
||||
const Frontpage = {
|
||||
oninit: function(vnode) {
|
||||
this.error = ''
|
||||
this.showAddLocation = false
|
||||
this.showAddLocation = true
|
||||
this.form = {
|
||||
city: '',
|
||||
zip: '',
|
||||
street_name: '',
|
||||
locations: [
|
||||
'Hverfisgata, 101 - Reykjavík',
|
||||
// 'Hverfisgata, 101 - Reykjavík',
|
||||
],
|
||||
type: [ true, false, false, false, false ],
|
||||
size: [ true, false, false, false, false ],
|
||||
|
@ -20,11 +23,23 @@ const Frontpage = {
|
|||
size: [ 'Alveg sama', '0 - 50fm', '50 - 80fm', '80 - 120fm', '120fm +'],
|
||||
rooms: [ 'Alveg sama', 'Stúdíó', '2 - 3 herb.', '3 - 4 herb.', '5 + herb.' ],
|
||||
}
|
||||
this.inputs = {
|
||||
zip: null,
|
||||
street: null,
|
||||
}
|
||||
this.cities = Object.keys(Constants.Locations)
|
||||
this.zips = Object.keys(Constants.Streets)
|
||||
this.streets = []
|
||||
},
|
||||
|
||||
onFormUpdate: function(vnode, key, index, event) {
|
||||
if (['city', 'zip', 'street_name'].includes(key)) {
|
||||
this.form[key] = event.target.value
|
||||
if (key === 'city') {
|
||||
this.zips = Constants.Locations[this.form.city] || []
|
||||
} else if (key === 'zip') {
|
||||
this.streets = Constants.Streets[this.form.zip] || []
|
||||
}
|
||||
} else if (key === 'type' || key === 'size' || key === 'rooms') {
|
||||
if (index > 0) {
|
||||
this.form[key][0] = false
|
||||
|
@ -58,6 +73,9 @@ const Frontpage = {
|
|||
this.form.zip = ''
|
||||
this.form.street_name = ''
|
||||
this.form.locations.push(entry)
|
||||
this.cities = Object.keys(Constants.Locations)
|
||||
this.zips = Object.keys(Constants.Streets)
|
||||
this.streets = []
|
||||
return false
|
||||
},
|
||||
|
||||
|
@ -99,30 +117,33 @@ const Frontpage = {
|
|||
hidden: !this.showAddLocation,
|
||||
onsubmit: (e) => this.onLocationAdd(e),
|
||||
}, [
|
||||
m('div.form-item', [
|
||||
m('label', 'City*'),
|
||||
m('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Reykjavík',
|
||||
oninput: (e) => this.onFormUpdate(vnode, 'city', null, e),
|
||||
}),
|
||||
]),
|
||||
m('div.form-item', [
|
||||
m('label', 'Postal code (optional)'),
|
||||
m('input', {
|
||||
type: 'text',
|
||||
placeholder: '000',
|
||||
oninput: (e) => this.onFormUpdate(vnode, 'zip', null, e),
|
||||
}),
|
||||
]),
|
||||
m('div.form-item.form-fill', [
|
||||
m('label', 'Street name (optional)'),
|
||||
m('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Enter your dream street adress',
|
||||
oninput: (e) => this.onFormUpdate(vnode, 'street_name', null, e),
|
||||
}),
|
||||
]),
|
||||
m(Combobox, {
|
||||
label: 'City*',
|
||||
items: this.cities,
|
||||
value: this.form.city,
|
||||
placeholder: 'Reykjavík',
|
||||
oninput: (e) => this.onFormUpdate(vnode, 'city', null, e),
|
||||
ondone: () => { this.inputs.zip.state.focus() },
|
||||
}),
|
||||
m(Combobox, {
|
||||
label: 'Postal code (optional)',
|
||||
items: this.zips,
|
||||
value: this.form.zip,
|
||||
placeholder: '000',
|
||||
oninput: (e) => this.onFormUpdate(vnode, 'zip', null, e),
|
||||
oncreate: (e) => { this.inputs.zip = e },
|
||||
ondone: () => { this.inputs.street.state.focus() },
|
||||
}),
|
||||
m(Combobox, {
|
||||
class: 'form-fill',
|
||||
label: 'Street name (optional)',
|
||||
items: this.streets,
|
||||
value: this.form.street_name,
|
||||
placeholder: 'Enter your dream street adress',
|
||||
oninput: (e) => this.onFormUpdate(vnode, 'street_name', null, e),
|
||||
oncreate: (e) => { this.inputs.street = e },
|
||||
ondone: () => { this.onLocationAdd(vnode) },
|
||||
}),
|
||||
m('div.form-item.form-small.form-no-label', [
|
||||
m('input', {
|
||||
class: this.form.city ? 'button-active' : 'button-outline',
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export function cancelPropagation(event) {
|
||||
event.stopPropagation()
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
101 Reykjavík Reykjavík (Miðborg) Þéttbýli Hagatorgi 1
|
||||
102 Reykjavík Reykjavík (Vatnsmýri og Skerjafjörður) Þéttbýli Hagatorgi 1
|
||||
103 Reykjavík Reykjavík (Háaleitis- og Bústaðahverfi) Þéttbýli Síðumúla 3-5, 108 Reykjavík
|
||||
104 Reykjavík Reykjavík (Laugardalur) Þéttbýli Síðumúla 3-5, 108 Reykjavík
|
||||
105 Reykjavík Reykjavík (Hlíðar) Þéttbýli Síðumúla 3-5, 108 Reykjavík
|
||||
107 Reykjavík Reykjavík (Vesturbær) Þéttbýli Hagatorgi 1
|
||||
108 Reykjavík Reykjavík (Múlar) Þéttbýli Síðumúla 3-5, 108 Reykjavík
|
||||
109 Reykjavík Reykjavík (Breiðholt) Þéttbýli Þönglabakka 4
|
||||
110 Reykjavík Reykjavík (Árbær) Þéttbýli Höfðabakka 9, 110 Reykjavík
|
||||
111 Reykjavík Reykjavík (Breiðholt) Þéttbýli Þönglabakka 4, 109 Reykjavík
|
||||
112 Reykjavík Reykjavík (Grafarvogur) Þéttbýli Höfðabakka 9, 110 Reykjavík
|
||||
113 Reykjavík Reykjavík (Grafarholt og Úlfarsárdalur) Þéttbýli Höfðabakka 9, 110 Reykjavík
|
||||
116 Reykjavík Reykjavík (Grundarhverfi) Þéttbýli Háholti 14, 270 Mosfellsbæ
|
||||
121 Reykjavík Reykjavík, pósthólf Pósthólf Pósthússtræti 5, 101 Reykjavík
|
||||
123 Reykjavík Reykjavík, pósthólf Pósthólf Síðumúla 3-5, 108 Reykjavík
|
||||
124 Reykjavík Reykjavík, pósthólf Pósthólf Síðumúla 3-5, 108 Reykjavík
|
||||
125 Reykjavík Reykjavík, pósthólf Pósthólf Síðumúla 3-5, 108 Reykjavík
|
||||
127 Reykjavík Reykjavík, pósthólf Pósthólf Eiðistorgi 15, 170 Seltjarnarnesi
|
||||
128 Reykjavík Reykjavík, pósthólf Pósthólf Síðumúli 3-5, 108 Reykjavík
|
||||
129 Reykjavík Reykjavík, pósthólf Pósthólf Þönglabakka 4, 109 Reykjavík
|
||||
130 Reykjavík Reykjavík, pósthólf Pósthólf Höfðabakka 9, 110 Reykjavík
|
||||
132 Reykjavík Reykjavík, pósthólf Pósthólf Hverafold 1-3, 112 Reykjavík
|
||||
150 Reykjavík Annað Opinberar stofnanir, eins og ráðuneyti og ríkisstofnanir.
|
||||
155 Reykjavík Annað Einkafyrirtæki, eins og viðskiptabankar.
|
||||
161 Reykjavík Reykjavík, dreifbýli (ofan Elliðavatns) Dreifbýli Höfðabakka 9, 110 Reykjavík
|
||||
162 Reykjavík - Dreifbýli Kjalarnes, dreifbýli Dreifbýli Höfðabakka 9, 110 Reykjavík
|
||||
170 Seltjarnarnesi Seltjarnarnes Þéttbýli Hagatorg 1
|
||||
172 Seltjarnarnesi Seltjarnarnes, pósthólf Pósthólf Hagatorg 1
|
||||
200 Kópavogi Kópavogur (Miðbær) Þéttbýli Dalvegi 18, 201 Kópavogi
|
||||
201 Kópavogi Kópavogur (Smárar, Lindir, Salir) Þéttbýli Dalvegi 18
|
||||
202 Kópavogi Kópavogur, pósthólf Pósthólf Dalvegi 18, 201 Kópavogi
|
||||
203 Kópavogi Kópavogur (Hvörf, Kórar) Þéttbýli Dalvegi 18, 201 Kópavogi
|
||||
206 Kópavogi Kópavogur, dreifbýli Dreifbýli Dalvegi 18
|
||||
210 Garðabæ Garðabær Þéttbýli Fjarðargötu 13-15, 220 Hafnarfirði
|
||||
212 Garðabæ Garðabær, pósthólf Pósthólf Fjarðargötu 13-15, 220 Hafnarfirði
|
||||
220 Hafnarfirði Hafnarfjörður (Miðbær) Þéttbýli Fjarðargötu 13-15
|
||||
221 Hafnarfirði Hafnarfjörður (Vellir) Þéttbýli Fjarðargötu 13-15, 220 Hafnarfirði
|
||||
222 Hafnarfirði Hafnarfjörður, pósthólf Pósthólf Fjarðargötu 13-15, 220 Hafnarfirði
|
||||
225 Garðabæ Garðabær (Álftanes) Þéttbýli Fjarðargötu 13-15, 220 Hafnarfirði
|
||||
270 Mosfellsbæ Mosfellsbær Þéttbýli Höfðabakka 9, 110 Reykjavík
|
||||
271 Mosfellsbæ Mosfellssveit, dreifbýli Dreifbýli Höfðabakka 9, 110 Reykjavík
|
||||
276 Mosfellsbæ Hvalfjörður og Kjós, dreifbýli Dreifbýli Höfðabakka 9, 110 Reykjavík
|
||||
190 Vogum Vogar Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
|
||||
191 Vogum Vatnsleysuströnd, dreifbýli Dreifbýli Hafnargötu 89, 230 Reykjanesbæ
|
||||
230 Reykjanesbæ Reykjanesbær (Keflavík) Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
|
||||
232 Reykjanesbæ Reykjanesbær, pósthólf Pósthólf Hafnargötu 89, 230 Reykjanesbæ
|
||||
233 Reykjanesbæ Reykjanesbær (Hafnir) Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
|
||||
235 Reykjanesbæ Keflavíkurflugvöllur Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
|
||||
240 Grindavík Grindavík Þéttbýli Víkurbraut 25
|
||||
241 Grindavík Grindavík, dreifbýli Dreifbýli Víkurbraut 25, 240 Grindavík
|
||||
245 Suðurnesjabæ Sandgerði Þéttbýli Suðurgötu 2-4
|
||||
246 Suðurnesjabæ Sandgerði, dreifbýli Dreifbýli Suðurgötu 2-4, 245 Sandgerði
|
||||
250 Suðurnesjabæ Garður Þéttbýli Garðbraut 69
|
||||
251 Suðurnesjabæ Garður, dreifbýli Dreifbýli Garðbraut 69, 250 Garði
|
||||
260 Reykjanesbæ Reykjanesbær (Njarðvík) Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
|
||||
262 Reykjanesbæ Reykjanesbær (Ásbrú) Þéttbýli Hafnargötu 89, 230 Reykjanesbæ
|
||||
300 Akranesi Akranes Þéttbýli Smiðjuvöllum 30
|
||||
301 Akranesi Akranes, dreifbýli Dreifbýli Smiðjuvöllum 30, 300 Akranesi
|
||||
302 Akranesi Akranes, pósthólf Pósthólf Smiðjuvöllum 30, 300 Akranesi
|
||||
310 Borgarnesi Borgarnes Þéttbýli Borgarbraut 12
|
||||
311 Borgarnesi Borgarnes, dreifbýli Dreifbýli Borgarbraut 12, 310 Borgarnesi
|
||||
320 Reykholti í Borgarfirði Reykholt í Borgarfirði, dreifbýli Dreifbýli Borgarbraut 12, 310 Borgarnesi
|
||||
340 Stykkishólmi Stykkishólmur Þéttbýli Aðalgötu 31
|
||||
341 Stykkishólmi Stykkishólmur, dreifbýli Dreifbýli Aðalgötu 31, 340 Stykkilshólmi
|
||||
342 Stykkishólmi Eyja og Miklaholtshreppur Dreifbýli Aðalgötu 31, 340 Stykkilshólmi
|
||||
345 Flatey á Breiðafirði Flatey á Breiðafirði Dreifbýli Aðalgötu 31, 340 Stykkishólmi
|
||||
350 Grundarfirði Grundarfjörður Þéttbýli Grundargötu 50
|
||||
351 Grundarfirði Grundarfjörður, dreifbýli Dreifbýli Grundargötu 50, 350 Grundarfirði
|
||||
355 Ólafsvík Ólafsvík Þéttbýli Bæjartúni 5
|
||||
356 Snæfellsbæ Snæfellsbær, dreifbýli Dreifbýli Bæjartúni 5, 355 Ólafsvík
|
||||
360 Hellissandi Hellissandur Þéttbýli Bæjartúni 5, 355 Ólafsvík
|
||||
370 Búðardal Búðardalur Þéttbýli Miðbraut 13
|
||||
371 Búðardal Búðardalur, dreifbýli Dreifbýli Miðbraut 13, 370 Búðardal
|
||||
380 Reykhólahreppi Reykhólar Þéttbýli Miðbraut 13, 370 Búðardal
|
||||
381 Reykhólahreppi Reykhólahreppur, dreifbýli Dreifbýli Miðbraut 13, 370 Búðardal
|
||||
400 Ísafirði Ísafjörður Þéttbýli Hafnarstræti 9-13
|
||||
401 Ísafirði Ísafjarðardjúp, dreifbýli (frá Ögri til Laugarholts) Dreifbýli Hafnarstræti 9-13, 400 Ísafirði
|
||||
410 Hnífsdal Hnífsdalur Þéttbýli Hafnarstræti 9-13, 400 Ísafirði
|
||||
415 Bolungarvík Bolungarvík Þéttbýli Aðalstræti 14
|
||||
416 Bolungarvík Bolungarvík, dreifbýli Dreifbýli Aðalstræti 14, 415 Bolungarvík
|
||||
420 Súðavík Súðavík Þéttbýli Grundarstræti 3-5
|
||||
421 Súðavík Súðavík, dreifbýli Dreifbýli Grundarstræti 3-5, Súðavík
|
||||
425 Flateyri Flateyri Þéttbýli Hafnarstræti 9-13, Ísafirði
|
||||
426 Flateyri Flateyri, dreifbýli Dreifbýli Hafnarstræti 9-13, Ísafirði
|
||||
430 Suðureyri Suðureyri Þéttbýli Hafnarstræti 9-13, Ísafirði
|
||||
431 Suðureyri Súgandafjörður, dreifbýli Dreifbýli Hafnarstræti 9-13, Ísafirði
|
||||
450 Patreksfirði Patreksfjörður Þéttbýli Bjarkargötu 4
|
||||
451 Patreksfirði Patreksfjörður, dreifbýli Dreifbýli Bjarkargötu 4, Patreksfirði
|
||||
460 Tálknafirði Tálknafjörður Þéttbýli Bjarkargötu 4, Patreksfirði
|
||||
461 Tálknafirði Tálknafjörður, dreifbýli Dreifbýli Bjarkargötu 4, Patreksfirði
|
||||
465 Bíldudal Bíldudalur Þéttbýli Bjarkargötu 4, Patreksfirði
|
||||
466 Bíldudal Bíldudalur, dreifbýli Dreifbýli Bjarkargötu 4, Patreksfirði
|
||||
470 Þingeyri Þingeyri Þéttbýli Hafnarstræti 9-13, Ísafirði
|
||||
471 Þingeyri Dýrafjörður, dreifbýli Dreifbýli Hafnarstræti 9-13, Ísafirði
|
||||
500 Stað Staður Dreifbýli Lækjargötu 2, 530 Hvammstanga
|
||||
510 Hólmavík Hólmavík Þéttbýli Hafnarbraut 19
|
||||
511 Hólmavík Hólmavík, dreifbýli Dreifbýli Hafnarbraut 19, 510 Hólmavík
|
||||
512 Hólmavík Ísafjarðardjúp, dreifbýli (nær Hólmavík) Dreifbýli Hafnarbraut 19, 510 Hólmavík
|
||||
520 Drangsnesi Drangsnes Þéttbýli Hafnarbraut 19, 510 Hólmavík
|
||||
524 Árneshreppi Árneshreppur Dreifbýli Hafnarbraut 19, 510 Hólmavík
|
||||
530 Hvammstanga Hvammstangi Þéttbýli Lækjargötu 2
|
||||
531 Hvammstanga Hvammstangi, dreifbýli Dreifbýli Lækjargötu 2, Hvammstanga
|
||||
540 Blönduósi Blönduós Þéttbýli Hnjúkabyggð 32
|
||||
541 Blönduósi Blönduós, dreifbýli Dreifbýli Hnjúkabyggð 32, Blönduósi
|
||||
545 Skagaströnd Skagaströnd Þéttbýli Höfða
|
||||
546 Skagaströnd Skagaströnd, dreifbýli Dreifbýli Hnjúkabyggð 32, Blönduósi
|
||||
550 Sauðárkróki Sauðárkrókur Þéttbýli Kirkjutorgi 5
|
||||
551 Sauðárkróki Sauðárkrókur, dreifbýli Dreifbýli Kirkjutorgi 5, Sauðárkróki
|
||||
560 Varmahlíð Varmahlíð Þéttbýli Kirkjutorgi 5, Sauðárkróki
|
||||
561 Varmahlíð Varmahlíð, dreifbýli Dreifbýli Kirkjutorgi 5, Sauðárkróki
|
||||
565 Hofsósi Hofsós Þéttbýli Kirkjutorgi 5, Sauðárkróki
|
||||
566 Hofsósi Hofsós, dreifbýli Dreifbýli Kirkjutorgi 5, Sauðárkróki
|
||||
570 Fljótum Fljót Dreifbýli Kirkjutorgi 5, Sauðárkróki
|
||||
580 Siglufirði Siglufjörður Þéttbýli Aðalgötu 24
|
||||
581 Siglufirði Siglufjörður, dreifbýli Dreifbýli Aðalgötu 24, Siglufirði
|
||||
600 Akureyri Akureyri Þéttbýli Strandgötu 3
|
||||
601 Akureyri Akureyri, dreifbýli Dreifbýli Strandgötu 3, 600 Akureyri
|
||||
602 Akureyri Akureyri, pósthólf Pósthólf Strandgötu 3, 600 Akureyri
|
||||
603 Akureyri Akureyri Þéttbýli Norðurtanga 3, 600 Akureyri
|
||||
604 Akureyri Akureyri, dreifbýli (Hörgársveit) Dreifbýli Norðurtanga 3
|
||||
605 Akureyri Akureyri, dreifbýli (Eyjafjarðarsveit) Dreifbýli Norðurtanga 3
|
||||
606 Akureyri Akureyri, dreifbýli (Svalbarðsströnd) Dreifbýli Norðurtanga 3
|
||||
607 Akureyri Akureyri, dreifbýli (Þingeyjarsveit) Dreifbýli Norðurtanga 3
|
||||
610 Grenivík Grenivík Þéttbýli Túngötu 3
|
||||
611 Grímsey Grímsey Þéttbýli Vallargata 9
|
||||
616 Grenivík Grenivík, dreifbýli Dreifbýli Túngötu 3, 610 Grenivík
|
||||
620 Dalvík Dalvík Þéttbýli Hafnarbraut 26
|
||||
621 Dalvík Dalvík, dreifbýli Dreifbýli Hafnarbraut 26, 620 Dalvík
|
||||
625 Ólafsfirði Ólafsfjörður Þéttbýli Aðalgötu 14
|
||||
626 Ólafsfirði Ólafsfjörður, dreifbýli Dreifbýli Aðalgötu 14, 625 Ólafsfirði
|
||||
630 Hrísey Hrísey Þéttbýli Norðurvegi 6-8
|
||||
640 Húsavík Húsavík Þéttbýli Garðarsbraut 70
|
||||
641 Húsavík Húsavík, dreifbýli Dreifbýli Garðarsbraut 70, 640 Húsavík
|
||||
645 Fosshóli Fosshóll, dreifbýli Dreifbýli Garðarsbraut 70, 640 Húsavík
|
||||
650 Laugum Laugar Þéttbýli Kjarna
|
||||
660 Mývatni Mývatn Dreifbýli Helluhrauni 3
|
||||
670 Kópaskeri Kópasker Þéttbýli Bakkagötu 2
|
||||
671 Kópaskeri Kópasker, dreifbýli Dreifbýli Bakkagötu 2, 670 Kópaskeri
|
||||
675 Raufarhöfn Raufarhöfn Þéttbýli Aðalbraut 19
|
||||
676 Raufarhöfn Raufarhöfn, dreifbýli Dreifbýli Aðalbraut 19, 675 Raufarhöfn
|
||||
680 Þórshöfn Þórshöfn Þéttbýli Fjarðarvegi 5
|
||||
681 Þórshöfn Þórshöfn, dreifbýli Dreifbýli Fjarðarvegi 5, 680 Þórshöfn
|
||||
685 Bakkafirði Bakkafjörður Þéttbýli Fjarðarvegi 5, 680 Þórshöfn
|
||||
686 Bakkafirði Bakkafjörður, dreifbýli Dreifbýli Fjarðarvegi 5, 680 Þórshöfn
|
||||
690 Vopnafirði Vopnafjörður Þéttbýli Kolbeinsgötu 10
|
||||
691 Vopnafirði Vopnafjörður, dreifbýli Dreifbýli Kolbeinsgötu 10, 690 Vopnafirði
|
||||
700 Egilsstöðum Egilsstaðir Þéttbýli Kaupvangi 6
|
||||
701 Egilsstöðum Egilsstaðir, dreifbýli Dreifbýli Kaupvangi 6, 700 Egilsstöðum
|
||||
710 Seyðisfirði Seyðisfjörður Þéttbýli Hafnargötu 4
|
||||
711 Seyðisfirði Seyðisfjörður, dreifbýli Dreifbýli Hafnargötu 6, 710 Seyðisfirði
|
||||
715 Mjóafirði Mjóifjörður, dreifbýli Dreifbýli Brekku
|
||||
720 Borgarfirði (eystri) Bakkagerði Þéttbýli Kaupvangi 6, 700 Egilsstöðum
|
||||
721 Borgarfirði (eystri) Borgarfjörður eystri Dreifbýli Kaupvangi 6, 700 Egilsstöðum
|
||||
730 Reyðarfirði Reyðarfjörður Þéttbýli Búðareyri 35
|
||||
731 Reyðarfirði Reyðarfjörður, dreifbýli Dreifbýli Búðareyri 35, 720 Reyðarfirði
|
||||
735 Eskifirði Eskifjörður Þéttbýli Strandgötu 55
|
||||
736 Eskifirði Eskifjörður, dreifbýli Dreifbýli Strandgötu 55, 735 Eskifirði
|
||||
740 Neskaupstað Neskaupstaður Þéttbýli Miðstræti 26
|
||||
741 Neskaupstað Neskaupstaður, dreifbýli Dreifbýli Miðstræti 26, 740 Neskaupstað
|
||||
750 Fáskrúðsfirði Fáskrúðsfjörður Þéttbýli Skólavegi 59
|
||||
751 Fáskrúðsfirði Fáskrúðsfjörður, dreifbýli Dreifbýli Skólavegi 59, 750, Fáskrúðsfirði
|
||||
755 Stöðvarfirði Stöðvarfjörður Þéttbýli Búðareyri 35, 720 Reiðarfirði
|
||||
756 Stöðvarfirði Stöðvarfjörður, dreifbýli Dreifbýli Búðareyri 35, 720 Reiðarfirði
|
||||
760 Breiðdalsvík Breiðdalsvík Þéttbýli Selnesi 38
|
||||
761 Breiðdalsvík Breiðdalsvík, dreifbýli Dreifbýli Selnesi 38, 760 Breiðdalsvík
|
||||
765 Djúpavogi Djúpivogur Þéttbýli Kambi 1
|
||||
766 Djúpavogi Djúpivogur, dreifbýli Dreifbýli Kambi 1, 765 Djúpavog
|
||||
780 Höfn í Hornafirði Höfn Þéttbýli Hafnarbraut 21
|
||||
781 Höfn í Hornafirði Höfn, dreifbýli Dreifbýli Hafnarbraut 21, 780 Höfn
|
||||
785 Öræfum Öræfi, dreifbýli Dreifbýli Hafnarbraut 21, 780 Höfn
|
||||
800 Selfossi Selfoss Þéttbýli Larsenstræti 1
|
||||
801 Selfossi Selfoss, dreifbyli (Árborg) Dreifbýli Larsenstræti 1, 800 Selfossi
|
||||
802 Selfossi Selfoss, pósthólf Pósthólf Larsenstræti 1, 800 Selfossi
|
||||
803 Selfossi Selfoss, dreifbýli (Flóahreppur) Dreifbýli Larsenstræti 1, 800 Selfossi
|
||||
804 Selfossi Selfoss, dreifbýli (Skeiða- og Gnúpverjahreppur) Dreifbýli Larsenstræti 1, 800 Selfossi
|
||||
805 Selfossi Selfoss, dreifbýli (Grímsnes- og Grafningshreppur) Dreifbýli Larsenstræti 1, 800 Selfossi
|
||||
806 Selfossi Selfoss (Bláskógabyggð) Dreifbýli Larsenstræti 1, 800 Selfossi
|
||||
810 Hveragerði Hveragerði Þéttbýli Sunnumörk 2-4
|
||||
815 Þorlákshöfn Þorlákshöfn Þéttbýli Hafnarberg 1
|
||||
816 Ölfusi Ölfus, dreifbýli Dreifbýli Larsenstræti 1, 800 Selfossi
|
||||
820 Eyrarbakka Eyrarbakki Þéttbýli Larsenstræti 1, 800 Selfossi
|
||||
825 Stokkseyri Stokkseyri Þéttbýli Larsenstræti 1, 800 Selfossi
|
||||
840 Laugarvatni Laugarvatn Þéttbýli Larsenstræti 1, 800 Selfossi
|
||||
845 Flúðum Flúðir Þéttbýli Larsenstræti 1, 800 Selfossi
|
||||
846 Flúðum Flúðir, dreifbýli Dreifbýli Larsenstræti 1, 800 Selfossi
|
||||
850 Hellu Hella Þéttbýli Þrúðvangi 10
|
||||
851 Hellu Hella, dreifbyli Dreifbýli Þrúðvangi 10, 850 Hella
|
||||
860 Hvolsvelli Hvolsvöllur Þéttbýli Austurvegi 2
|
||||
861 Hvolsvelli Hvolsvöllur, dreifbýli Dreifbýli Austurvegi 2, 860 Hvolsvelli
|
||||
870 Vík Vík Þéttbýli Austurvegi 2, 860 Hvolsvelli
|
||||
871 Vík Vík, dreifbýli Dreifbýli Austurvegi 2, 860 Hvolsvelli
|
||||
880 Kirkjubæjarklaustri Kirkjubæjarklaustur Þéttbýli Austurvegi 2, 860 Hvolsvelli
|
||||
881 Kirkjubæjarklaustri Kirkjubæjarklaustur, dreifbýli Dreifbýli Austurvegi 2, 860 Hvolsvelli
|
||||
900 Vestmannaeyjum Vestmannaeyjar Þéttbýli Vestmannabraut 22
|
||||
902 Vestmannaeyjum Vestmannaeyjar, pósthólf Pósthólf Vestmannabraut 22, 900 Vestmannaeyjar
|
|
@ -50,7 +50,7 @@
|
|||
"dot": "^2.0.0-beta.1",
|
||||
"flaska": "^1.3.0",
|
||||
"formidable": "^1.2.6",
|
||||
"msnodesqlv8": "^2.4.7",
|
||||
"msnodesqlv8": "^2.7.0",
|
||||
"nconf-lite": "^2.0.0",
|
||||
"striptags": "^3.2.0"
|
||||
},
|
||||
|
|
|
@ -571,6 +571,38 @@ i.ic-plus {
|
|||
background-size: contain;
|
||||
}
|
||||
|
||||
/* ---------------- icons ---------------- */
|
||||
|
||||
.combobox {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.combobox-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-radius: var(--form-radius);
|
||||
border: var(--form-border);
|
||||
background: var(--main-bg);
|
||||
z-index: 5;
|
||||
max-height: 530px;
|
||||
overflow-y: scroll;
|
||||
box-shadow: 0px 3px 10px #0004;
|
||||
}
|
||||
|
||||
.combobox-list-item {
|
||||
padding: 0.9rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.combobox-list-item:hover,
|
||||
.combobox-list-item:active,
|
||||
.combobox-list-item:focus {
|
||||
background: var(--main-accent);
|
||||
color: var(--main-accent-fg);
|
||||
}
|
||||
|
||||
</style>
|
||||
<link id="headstyle" rel="Stylesheet" href="/assets/app.css?v=2" type="text/css" media="none" />
|
||||
</head>
|
||||
|
|
|
@ -8,17 +8,13 @@
|
|||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --experimental-modules index.mjs",
|
||||
"start": "node index.mjs",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build:prod": "asbundle app/index.js public/assets/app.js && asbundle app/admin/admin.js public/assets/admin.js",
|
||||
"build": "asbundle app/index.js public/assets/app.js && asbundle app/admin/admin.js public/assets/admin.js",
|
||||
"dev:build": "npm-watch build",
|
||||
"dev:server": "node index.mjs | bunyan",
|
||||
"dev": "npm-watch dev:server",
|
||||
"watch:sass:public": "sass --watch app/app.scss public/assets/app.css",
|
||||
"watch:sass:admin": "sass --watch app/admin.scss public/assets/admin.css",
|
||||
"prod": "npm run build && npm start",
|
||||
"temp": "asbundle"
|
||||
"dev": "npm-watch dev:server"
|
||||
},
|
||||
"watch": {
|
||||
"dev:server": {
|
||||
|
|