Compare commits

...

4 commits

Author SHA1 Message Date
a5c7e53802 base: Add feature flag for making articles api private
Some checks failed
/ deploy (push) Failing after -75h28m39s
2023-11-09 09:46:43 +00:00
d1730974dc nfp_moe: cleanup 2023-11-09 09:46:43 +00:00
4216e036e4 heimaerbest: dev 2023-11-09 09:45:37 +00:00
825cd7ef2d filadelfia_web: init 2023-11-09 09:45:37 +00:00
32 changed files with 132760 additions and 33 deletions

View file

@ -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', [
@ -111,6 +114,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)

View file

@ -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',

View file

@ -0,0 +1,24 @@
import fs from 'fs/promises'
import dot from 'dot'
import Parent from '../base/serve.mjs'
import path from 'path'
export default class ServeHandler extends Parent {
loadTemplate(indexFile) {
this.template = dot.template(indexFile.toString(), { argName: [
'siteUrl',
] })
}
async serveIndex(ctx) {
if (process.env.NODE_ENV !== 'production') {
let indexFile = await fs.readFile(path.join(this.root, 'index.html'))
this.loadTemplate(indexFile)
}
ctx.body = this.template({
siteUrl: this.frontend + ctx.url,
})
ctx.type = 'text/html; charset=utf-8'
}
}

View file

@ -0,0 +1,26 @@
import config from '../base/config.mjs'
import Parent from '../base/server.mjs'
import StaticRoutes from '../base/static_routes.mjs'
import ServeHandler from './serve.mjs'
import AuthenticationRoutes from '../base/authentication/routes.mjs'
import VideoRoutes from './video/routes.mjs'
export default class Server extends Parent {
init() {
super.init()
let localUtil = new this.core.sc.Util(import.meta.url)
delete this.flaskaOptions.appendHeaders['Content-Security-Policy']
this.flaskaOptions.nonce = []
this.routes = {
static: new StaticRoutes(),
auth: new AuthenticationRoutes(),
videos: new VideoRoutes(),
}
this.routes.serve = new ServeHandler({
root: localUtil.getPathFromRoot('../public'),
version: this.core.app.running,
frontend: config.get('frontend:url'),
})
}
}

View file

@ -0,0 +1,36 @@
import config from '../../base/config.mjs'
import Client from '../../base/media/client.mjs'
import { deleteFile, uploadFile } from '../../base/media/upload.mjs'
import { parseVideos, parseVideo } from './util.mjs'
export default class VideoRoutes {
constructor(opts = {}) {
Object.assign(this, {
uploadFile: uploadFile,
deleteFile: deleteFile,
})
}
register(server) {
server.flaska.get('/api/videos', server.authenticate(), this.getVideos.bind(this))
server.flaska.get('/api/videos/uploadToken', server.authenticate(), this.getUploadToken.bind(this))
}
/** GET: /api/videos */
async getVideos(ctx) {
console.log(ctx.state.auth_token)
let res = await ctx.db.safeCallProc('filadelfia_archive.videos_auth_get_all', [ctx.state.auth_token])
ctx.body = {
videos: parseVideos(res.results[0]),
}
}
async getUploadToken(ctx) {
const media = config.get('media')
return {
token: Client.createJwt({ iss: media.iss }, media.secret),
}
}
}

View file

@ -0,0 +1,16 @@
import { parseMediaAndBanner } from "../../base/util.mjs"
export function parseVideos(videos) {
for (let i = 0; i < videos.length; i++) {
parseVideo(videos[i])
}
return videos
}
export function parseVideo(video) {
if (!video) {
return null
}
parseMediaAndBanner(video)
return video
}

53
filadelfia_web/app/api.js Normal file
View file

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

View file

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

View file

@ -0,0 +1,41 @@
const m = require('mithril')
const videos = require('./videos')
const Authentication = require('./authentication')
const Menu = {
oninit: function(vnode) {
this.currentActive = 'home'
this.loading = false
this.onbeforeupdate()
},
onbeforeupdate: function() {
let currentPath = m.route.get()
/*if (currentPath === '/') this.currentActive = 'home'
else if (currentPath === '/login') this.currentActive = 'login'
else if (currentPath && currentPath.startsWith('/page')) this.currentActive = currentPath.slice(currentPath.lastIndexOf('/') + 1)*/
},
logOut: function() {
Authentication.clearToken()
m.route.set('/')
},
view: function() {
console.log(Authentication.currentUser)
return Authentication.currentUser
? [
m('nav', [
m('h4', m(m.route.Link, { href: '/browse' }, 'Filadelfia archival center')),
Authentication.currentUser.rank > 10
? m(m.route.Link, { class: 'upload', href: '/upload' }, 'Upload')
: null,
m('button.logout', { onclick: this.logOut }, 'Log out'),
])
]
: null
},
}
module.exports = Menu

View file

@ -0,0 +1,46 @@
const m = require('mithril')
const Authentication = require('./authentication')
const Header = require('./header')
const Login = require('./page_login')
const Browse = require('./page_browse')
window.m = m
var fileref = document.createElement("link");
fileref.setAttribute("rel", "stylesheet");
fileref.setAttribute("type", "text/css");
fileref.setAttribute("href", '/assets/app.css?v=2');
document.head.appendChild(fileref)
m.route.setOrig = m.route.set
m.route.set = function(path, data, options){
m.route.setOrig(path, data, options)
window.scrollTo(0, 0)
}
m.route.linkOrig = m.route.link
m.route.link = function(vnode){
m.route.linkOrig(vnode)
window.scrollTo(0, 0)
}
m.route.prefix = ''
const allRoutes = {
'/': Login,
'/browse': Browse,
}
// Wait until we finish checking avif support, some views render immediately and will ask for this immediately before the callback gets called.
/*
* imgsupport.js from leechy/imgsupport
*/
const AVIF = new Image();
AVIF.onload = AVIF.onerror = function () {
window.supportsavif = (AVIF.height === 2)
document.body.className = document.body.className + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly')
m.mount(document.getElementById('header'), Header)
m.route(document.getElementById('main'), '/', allRoutes)
}
AVIF.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAABcAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAABAA0ABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKCBgAPkgIaDQgMgkf8AAAQAAAr3A=';

View file

@ -0,0 +1,31 @@
const m = require('mithril')
const Authentication = require('./authentication')
const videos = require('./videos')
const Browse = {
oninit: function(vnode) {
if (!videos.Tree.length) {
this.refreshTree()
}
},
oncreate: function() {
if (Authentication.currentUser) return
},
refreshTree: function(vnode) {
videos.refreshTree()
},
view: function(vnode) {
return [
videos.error
? m('div.full-error', { onclick: this.refreshTree.bind(this) }, [
videos.error, m('br'), 'Click here to try again'
])
: null,
]
},
}
module.exports = Browse

View file

@ -0,0 +1,91 @@
const m = require('mithril')
const Authentication = require('./authentication')
const api = require('./api')
const Login = {
oninit: function(vnode) {
this.redirect = vnode.attrs.redirect || ''
if (Authentication.currentUser) return m.route.set('/browse')
this.error = ''
this.loading = false
this.username = ''
this.password = ''
},
oncreate: function() {
if (Authentication.currentUser) return
},
loginuser: function(vnode, e) {
e.preventDefault()
this.error = ''
if (!this.password) this.error = 'Password is missing'
if (!this.username) this.error = 'Email is missing'
if (this.error) return false
this.loading = true
api.sendRequest({
method: 'POST',
url: '/api/authentication/login',
body: {
email: this.username,
password: this.password,
},
})
.then((result) => {
if (!result.token) {
return Promise.reject(new Error('Server authentication down.'))
}
Authentication.updateToken(result.token)
m.route.set(this.redirect || '/browse')
})
.catch((error) => {
this.error = 'Error while logging in! ' + error.message
vnode.state.password = ''
})
.then(() => {
this.loading = false
m.redraw()
})
return false
},
view: function(vnode) {
return [
m('div.modal', [
m('form', {
onsubmit: this.loginuser.bind(this, vnode),
}, [
m('h3', 'Filadelfia archival center'),
this.error ? m('p.error', this.error) : null,
m('label', 'Email or name'),
m('input', {
type: 'text',
value: this.username,
oninput: (e) => { this.username = e.currentTarget.value },
}),
m('label', 'Password'),
m('input', {
type: 'password',
value: this.password,
oninput: (e) => { this.password = e.currentTarget.value },
}),
m('input.spinner', {
hidden: this.loading,
type: 'submit',
value: 'Log in',
}),
this.loading ? m('div.loading-spinner') : null,
]),
m('div.login--asuna.spritesheet'),
]),
]
},
}
module.exports = Login

View file

@ -0,0 +1,91 @@
const m = require('mithril')
const Authentication = require('./authentication')
const api = require('./api')
const Login = {
oninit: function(vnode) {
this.redirect = vnode.attrs.redirect || ''
if (!Authentication.currentUser) return m.route.set('/')
this.error = ''
this.loading = false
this.body = {
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
title: '',
file: null,
}
},
uploadfile: function(vnode, e) {
e.preventDefault()
this.error = ''
if (!this.password) this.error = 'Password is missing'
if (!this.username) this.error = 'Email is missing'
if (this.error) return false
this.loading = true
api.sendRequest({
method: 'POST',
url: '/api/authentication/login',
body: {
email: this.username,
password: this.password,
},
})
.then((result) => {
if (!result.token) {
return Promise.reject(new Error('Server authentication down.'))
}
Authentication.updateToken(result.token)
m.route.set(this.redirect || '/browse')
})
.catch((error) => {
this.error = 'Error while logging in! ' + error.message
vnode.state.password = ''
})
.then(() => {
this.loading = false
m.redraw()
})
return false
},
view: function(vnode) {
return [
m('div.modal', [
m('form', {
onsubmit: this.uploadfile.bind(this, vnode),
}, [
m('h3', 'Filadelfia archival center'),
this.error ? m('p.error', this.error) : null,
m('label', 'Email or name'),
m('input', {
type: 'text',
value: this.username,
oninput: (e) => { this.username = e.currentTarget.value },
}),
m('label', 'Password'),
m('input', {
type: 'password',
value: this.password,
oninput: (e) => { this.password = e.currentTarget.value },
}),
m('input.spinner', {
hidden: this.loading,
type: 'submit',
value: 'Log in',
}),
this.loading ? m('div.loading-spinner') : null,
]),
m('div.login--asuna.spritesheet'),
]),
]
},
}
module.exports = Login

View file

@ -0,0 +1,35 @@
const m = require('mithril')
const api = require('./api')
const Tree = []
exports.Tree = Tree
exports.loading = false
exports.error = ''
exports.refreshTree = function() {
exports.error = ''
if (exports.loading) return Promise.resolve()
exports.loading = true
m.redraw()
return api.sendRequest({
method: 'GET',
url: '/api/videos',
})
.then(pages => {
console.log(pages)
Tree.splice(0, Tree.length)
Tree.push.apply(Tree, pages.videos)
exports.loading = false
m.redraw()
}, err => {
exports.loading = false
m.redraw()
exports.error = 'Error fetching videos: ' + err.message
})
}

1
filadelfia_web/base Symbolic link
View file

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

View file

@ -0,0 +1,8 @@
{
"scripts": {
"build": "echo done;"
},
"dependencies": {
"service-core": "^3.0.0-beta.17"
}
}

24
filadelfia_web/dev.mjs Normal file
View file

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

11
filadelfia_web/index.mjs Normal file
View file

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

View file

@ -0,0 +1,61 @@
{
"name": "filadelfia_web",
"version": "1.0.0",
"port": 4130,
"description": "Filadelfia web portal",
"main": "index.js",
"directories": {
"test": "test"
},
"scripts": {
"start": "node index.mjs",
"test": "echo \"Error: no test specified\" && exit 1",
"build:prod": "asbundle app/index.js public/assets/app.js",
"build": "asbundle app/index.js public/assets/app.js",
"dev:build": "eltro --watch build --npm build",
"dev:server": "eltro --watch server --npm server",
"server": "node dev.mjs | bunyan"
},
"watch": {
"server": {
"patterns": [
"api",
"base",
"../base"
],
"extensions": "js,mjs",
"quiet": true,
"inherit": true
},
"build": {
"patterns": [
"app"
],
"extensions": "js",
"quiet": true,
"inherit": true
}
},
"repository": {
"type": "git",
"url": "https://git.nfp.is/nfp/nfp_sites.git"
},
"author": "Jonatan Nilsson",
"license": "WTFPL",
"bugs": {
"url": "https://git.nfp.is/nfp/nfp_sites/issues"
},
"homepage": "https://git.nfp.is/nfp/nfp_sites",
"dependencies": {
"dot": "^2.0.0-beta.1",
"msnodesqlv8": "^2.4.7",
"nconf-lite": "^2.0.0"
},
"devDependencies": {
"asbundle": "^2.6.1",
"eltro": "^1.4.4",
"flaska": "^1.3.2",
"mithril": "^2.2.2",
"service-core": "^3.0.0-beta.17"
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,246 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Filadelfia web portal</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/favicon.png">
<style>
[hidden] { display: none !important; }
:root {
--bg: #fff;
--bg-component: #f3f7ff;
--bg-component-alt: #ffd99c;
--color: #031131;
--main: #1066ff;
--main-fg: #fff;
--error: red;
--error-bg: hsl(0, 75%, 80%);
}
/* Box sizing rules */
*, *::before, *::after { box-sizing: border-box;
}
/* Remove default margin */
body, h1, h2, h3, h4, p, figure, blockquote, dl, dd {
margin: 0;
}
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
line-height: 1.5;
font-size: 16px;
font-family: 'Inter var', Helvetica, Arial, sans-serif;
font-variation-settings: "slnt" 0;
font-feature-settings: "case", "frac", "tnum", "ss02", "calt", "ccmp", "kern";
background: var(--bg);
color: var(--color);
display: flex;
flex-direction: column;
}
.italic { font-variation-settings: "slnt" 10deg; }
input, button, textarea, select {
font: inherit;
}
h1 {
font-size: 1.88rem;
}
h2 {
font-size: 1.66rem;
}
h3 {
font-size: 1.44rem;
}
h4 {
font-size: 1.22rem;
}
h5 {
font-size: 1.0rem;
}
a, a:visited, button {
text-decoration: underline;
border: none;
padding: 0;
margin: 0;
font-weight: bold;
cursor: pointer;
color: var(--main);
background: transparent;
}
input[type=text],
input[type=password] {
border: 1px solid var(--main);
background: #fff;
color: var(--color);
border-radius: 0;
padding: 0.25rem;
line-height: 1rem;
outline: none;
width: 100%;
}
.button, input[type=submit] {
background: var(--main);
color:var(--main-fg);
border-radius: 10px;
padding: 0.25rem 1rem;
border: none;
margin: 1rem 0 2rem;
align-self: center;
cursor: pointer;
}
.button.spinner, input[type=submit].spinner {
height: 2rem;
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
input[type=text]:focus {
outline: 1px solid var(--link);
}
h1 {
margin-bottom: 1rem;
}
#main {
flex: 2 1 auto;
display: flex;
flex-direction: column;
}
.modal {
flex: 2 1 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.modal h3 {
text-align: center;
margin-bottom: 1rem;
}
form p, label {
font-size: 0.75rem;
font-weight: 500;
margin: 0.5rem 0;
display: block;
}
.error {
color: var(--error);
}
.modal form {
background: var(--bg-component);
border-radius: 20px;
width: 100%;
max-width: 500px;
margin: 2rem;
padding: 1rem;
display: flex;
flex-direction: column;
}
.loading-spinner {
display: inline-block;
width: 80px;
height: 80px;
align-self: center;
}
.loading-spinner:after {
content: " ";
display: block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
border: 6px solid var(--main);
border-color: var(--main) transparent var(--main) transparent;
animation: loading-spinner 1.2s linear infinite;
}
@keyframes loading-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Nav */
#header nav {
display: flex;
background: var(--bg-component);
padding: 0.5rem 1rem 0.5rem 0;
}
#header nav a,
#header nav button {
margin-left: 1rem;
}
#header h4 {
flex: 2 1 auto;
}
#header h4 a,
#header h4 a:visited {
color: var(--color);
}
#header .logout,
#header .upload {
padding: 0.25rem 1.5rem;
border-radius: 2rem;
text-decoration: none;
}
#header .logout {
background: var(--bg-component-alt);
color: var(--color);
}
#header .upload {
background: var(--main);
color: var(--main-fg);
}
/* Main */
.full-error {
background: var(--error-bg);
color: var(--color);
font-size: 0.8rem;
text-align: center;
padding: 0.25rem;
cursor: pointer;
flex: 2 1 auto;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<body>
<div id="header"></div>
<main id="main"></main>
<script type="text/javascript" src="/assets/app.js?v=2"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

144
heimaerbest/app/combobox.js Normal file
View file

@ -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

272
heimaerbest/app/consts.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -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
heimaerbest/app/temp.txt Normal file
View file

4
heimaerbest/app/util.js Normal file
View file

@ -0,0 +1,4 @@
export function cancelPropagation(event) {
event.stopPropagation()
return false
}

195
heimaerbest/app/zipcode.txt Normal file
View file

@ -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

View file

@ -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"
},

View file

@ -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>

View file

@ -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"
},
"watch": {
"dev:server": {