Add progressive web app companion for cross-platform access

Vite + TypeScript PWA that mirrors the Android app's core features:
- Pre-processed shelter data (build-time UTM33N→WGS84 conversion)
- Leaflet map with shelter markers, user location, and offline tiles
- Canvas compass arrow (ported from DirectionArrowView.kt)
- IndexedDB shelter cache with 7-day staleness check
- Service worker with CacheFirst tiles and precached app shell
- i18n for en, nb, nn (ported from Android strings.xml)
- iOS/Android compass handling with low-pass filter
- Respects user map interaction (no auto-snap on pan/zoom)
- Build revision cache-breaker for reliable SW updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-08 17:41:38 +01:00
commit e8428de775
12051 changed files with 1799735 additions and 0 deletions

View file

@ -0,0 +1,246 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var leven = _interopDefault(require('leven'));
var pointer = _interopDefault(require('jsonpointer'));
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
var AJV_ERROR_KEYWORD_WEIGHT_MAP = {
"enum": 1,
type: 0
};
var QUOTES_REGEX = /"/g;
var NOT_REGEX = /NOT/g;
var SLASH_REGEX = /\//g;
var filterSingleErrorPerProperty = function filterSingleErrorPerProperty(errors) {
var errorsPerProperty = errors.reduce(function (acc, error) {
var _ref, _error$params$additio, _error$params, _error$params2, _AJV_ERROR_KEYWORD_WE, _AJV_ERROR_KEYWORD_WE2;
var prop = error.instancePath + ((_ref = (_error$params$additio = (_error$params = error.params) == null ? void 0 : _error$params.additionalProperty) != null ? _error$params$additio : (_error$params2 = error.params) == null ? void 0 : _error$params2.missingProperty) != null ? _ref : '');
var existingError = acc[prop];
if (!existingError) {
acc[prop] = error;
return acc;
}
var weight = (_AJV_ERROR_KEYWORD_WE = AJV_ERROR_KEYWORD_WEIGHT_MAP[error.keyword]) != null ? _AJV_ERROR_KEYWORD_WE : 0;
var existingWeight = (_AJV_ERROR_KEYWORD_WE2 = AJV_ERROR_KEYWORD_WEIGHT_MAP[existingError.keyword]) != null ? _AJV_ERROR_KEYWORD_WE2 : 0;
if (weight > existingWeight) {
acc[prop] = error;
}
return acc;
}, {});
return Object.values(errorsPerProperty);
};
var getSuggestion = function getSuggestion(_ref) {
var value = _ref.value,
suggestions = _ref.suggestions,
_ref$format = _ref.format,
format = _ref$format === void 0 ? function (suggestion) {
return "Did you mean '" + suggestion + "'?";
} : _ref$format;
if (!value) return '';
var bestSuggestion = suggestions.reduce(function (best, current) {
var distance = leven(value, current);
if (best.distance > distance) {
return {
value: current,
distance: distance
};
}
return best;
}, {
distance: Infinity,
value: ''
});
return bestSuggestion.distance < value.length ? format(bestSuggestion.value) : '';
};
var pointerToDotNotation = function pointerToDotNotation(pointer) {
return pointer.replace(SLASH_REGEX, '.');
};
var cleanAjvMessage = function cleanAjvMessage(message) {
return message.replace(QUOTES_REGEX, "'").replace(NOT_REGEX, 'not');
};
var getLastSegment = function getLastSegment(path) {
var segments = path.split('/');
return segments.pop();
};
var safeJsonPointer = function safeJsonPointer(_ref) {
var object = _ref.object,
pnter = _ref.pnter,
fallback = _ref.fallback;
try {
return pointer.get(object, pnter);
} catch (err) {
return fallback;
}
};
var betterAjvErrors = function betterAjvErrors(_ref) {
var errors = _ref.errors,
data = _ref.data,
schema = _ref.schema,
_ref$basePath = _ref.basePath,
basePath = _ref$basePath === void 0 ? '{base}' : _ref$basePath;
if (!Array.isArray(errors) || errors.length === 0) {
return [];
}
var definedErrors = filterSingleErrorPerProperty(errors);
return definedErrors.map(function (error) {
var path = pointerToDotNotation(basePath + error.instancePath);
var prop = getLastSegment(error.instancePath);
var defaultContext = {
errorType: error.keyword
};
var defaultMessage = (prop ? "property '" + prop + "'" : path) + " " + cleanAjvMessage(error.message);
var validationError;
switch (error.keyword) {
case 'additionalProperties':
{
var additionalProp = error.params.additionalProperty;
var suggestionPointer = error.schemaPath.replace('#', '').replace('/additionalProperties', '');
var _safeJsonPointer = safeJsonPointer({
object: schema,
pnter: suggestionPointer,
fallback: {
properties: {}
}
}),
properties = _safeJsonPointer.properties;
validationError = {
message: "'" + additionalProp + "' property is not expected to be here",
suggestion: getSuggestion({
value: additionalProp,
suggestions: Object.keys(properties != null ? properties : {}),
format: function format(suggestion) {
return "Did you mean property '" + suggestion + "'?";
}
}),
path: path,
context: defaultContext
};
break;
}
case 'enum':
{
var suggestions = error.params.allowedValues.map(function (value) {
return value.toString();
});
var _prop = getLastSegment(error.instancePath);
var value = safeJsonPointer({
object: data,
pnter: error.instancePath,
fallback: ''
});
validationError = {
message: "'" + _prop + "' property must be equal to one of the allowed values",
suggestion: getSuggestion({
value: value,
suggestions: suggestions
}),
path: path,
context: _extends({}, defaultContext, {
allowedValues: error.params.allowedValues
})
};
break;
}
case 'type':
{
var _prop2 = getLastSegment(error.instancePath);
var type = error.params.type;
validationError = {
message: "'" + _prop2 + "' property type must be " + type,
path: path,
context: defaultContext
};
break;
}
case 'required':
{
validationError = {
message: path + " must have required property '" + error.params.missingProperty + "'",
path: path,
context: defaultContext
};
break;
}
case 'const':
{
return {
message: "'" + prop + "' property must be equal to the allowed value",
path: path,
context: _extends({}, defaultContext, {
allowedValue: error.params.allowedValue
})
};
}
default:
return {
message: defaultMessage,
path: path,
context: defaultContext
};
} // Remove empty properties
var errorEntries = Object.entries(validationError);
for (var _i = 0, _errorEntries = errorEntries; _i < _errorEntries.length; _i++) {
var _errorEntries$_i = _errorEntries[_i],
key = _errorEntries$_i[0],
_value = _errorEntries$_i[1];
if (_value === null || _value === undefined || _value === '') {
delete validationError[key];
}
}
return validationError;
});
};
exports.betterAjvErrors = betterAjvErrors;
//# sourceMappingURL=better-ajv-errors.cjs.development.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
"use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(exports,"__esModule",{value:!0});var t=e(require("leven")),r=e(require("jsonpointer"));function a(){return(a=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(e[a]=r[a])}return e}).apply(this,arguments)}var n={enum:1,type:0},o=/"/g,s=/NOT/g,u=/\//g,l=function(e){var r=e.value,a=e.format,n=void 0===a?function(e){return"Did you mean '"+e+"'?"}:a;if(!r)return"";var o=e.suggestions.reduce((function(e,a){var n=t(r,a);return e.distance>n?{value:a,distance:n}:e}),{distance:Infinity,value:""});return o.distance<r.length?n(o.value):""},i=function(e){return e.split("/").pop()},p=function(e){var t=e.object,a=e.pnter,n=e.fallback;try{return r.get(t,a)}catch(e){return n}};exports.betterAjvErrors=function(e){var t=e.errors,r=e.data,c=e.schema,d=e.basePath,v=void 0===d?"{base}":d;return Array.isArray(t)&&0!==t.length?function(e){var t=e.reduce((function(e,t){var r,a,o,s,u,l,i=t.instancePath+(null!=(r=null!=(a=null==(o=t.params)?void 0:o.additionalProperty)?a:null==(s=t.params)?void 0:s.missingProperty)?r:""),p=e[i];return p?((null!=(u=n[t.keyword])?u:0)>(null!=(l=n[p.keyword])?l:0)&&(e[i]=t),e):(e[i]=t,e)}),{});return Object.values(t)}(t).map((function(e){var t,n=function(e){return e.replace(u,".")}(v+e.instancePath),d=i(e.instancePath),y={errorType:e.keyword},f=(d?"property '"+d+"'":n)+" "+e.message.replace(o,"'").replace(s,"not");switch(e.keyword){case"additionalProperties":var m=e.params.additionalProperty,g=e.schemaPath.replace("#","").replace("/additionalProperties",""),h=p({object:c,pnter:g,fallback:{properties:{}}}).properties;t={message:"'"+m+"' property is not expected to be here",suggestion:l({value:m,suggestions:Object.keys(null!=h?h:{}),format:function(e){return"Did you mean property '"+e+"'?"}}),path:n,context:y};break;case"enum":var b=e.params.allowedValues.map((function(e){return e.toString()})),P=i(e.instancePath),w=p({object:r,pnter:e.instancePath,fallback:""});t={message:"'"+P+"' property must be equal to one of the allowed values",suggestion:l({value:w,suggestions:b}),path:n,context:a({},y,{allowedValues:e.params.allowedValues})};break;case"type":t={message:"'"+i(e.instancePath)+"' property type must be "+e.params.type,path:n,context:y};break;case"required":t={message:n+" must have required property '"+e.params.missingProperty+"'",path:n,context:y};break;case"const":return{message:"'"+d+"' property must be equal to the allowed value",path:n,context:a({},y,{allowedValue:e.params.allowedValue})};default:return{message:f,path:n,context:y}}for(var j=0,k=Object.entries(t);j<k.length;j++){var x=k[j],O=x[1];null!=O&&""!==O||delete t[x[0]]}return t})):[]};
//# sourceMappingURL=better-ajv-errors.cjs.production.min.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,240 @@
import leven from 'leven';
import pointer from 'jsonpointer';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
var AJV_ERROR_KEYWORD_WEIGHT_MAP = {
"enum": 1,
type: 0
};
var QUOTES_REGEX = /"/g;
var NOT_REGEX = /NOT/g;
var SLASH_REGEX = /\//g;
var filterSingleErrorPerProperty = function filterSingleErrorPerProperty(errors) {
var errorsPerProperty = errors.reduce(function (acc, error) {
var _ref, _error$params$additio, _error$params, _error$params2, _AJV_ERROR_KEYWORD_WE, _AJV_ERROR_KEYWORD_WE2;
var prop = error.instancePath + ((_ref = (_error$params$additio = (_error$params = error.params) == null ? void 0 : _error$params.additionalProperty) != null ? _error$params$additio : (_error$params2 = error.params) == null ? void 0 : _error$params2.missingProperty) != null ? _ref : '');
var existingError = acc[prop];
if (!existingError) {
acc[prop] = error;
return acc;
}
var weight = (_AJV_ERROR_KEYWORD_WE = AJV_ERROR_KEYWORD_WEIGHT_MAP[error.keyword]) != null ? _AJV_ERROR_KEYWORD_WE : 0;
var existingWeight = (_AJV_ERROR_KEYWORD_WE2 = AJV_ERROR_KEYWORD_WEIGHT_MAP[existingError.keyword]) != null ? _AJV_ERROR_KEYWORD_WE2 : 0;
if (weight > existingWeight) {
acc[prop] = error;
}
return acc;
}, {});
return Object.values(errorsPerProperty);
};
var getSuggestion = function getSuggestion(_ref) {
var value = _ref.value,
suggestions = _ref.suggestions,
_ref$format = _ref.format,
format = _ref$format === void 0 ? function (suggestion) {
return "Did you mean '" + suggestion + "'?";
} : _ref$format;
if (!value) return '';
var bestSuggestion = suggestions.reduce(function (best, current) {
var distance = leven(value, current);
if (best.distance > distance) {
return {
value: current,
distance: distance
};
}
return best;
}, {
distance: Infinity,
value: ''
});
return bestSuggestion.distance < value.length ? format(bestSuggestion.value) : '';
};
var pointerToDotNotation = function pointerToDotNotation(pointer) {
return pointer.replace(SLASH_REGEX, '.');
};
var cleanAjvMessage = function cleanAjvMessage(message) {
return message.replace(QUOTES_REGEX, "'").replace(NOT_REGEX, 'not');
};
var getLastSegment = function getLastSegment(path) {
var segments = path.split('/');
return segments.pop();
};
var safeJsonPointer = function safeJsonPointer(_ref) {
var object = _ref.object,
pnter = _ref.pnter,
fallback = _ref.fallback;
try {
return pointer.get(object, pnter);
} catch (err) {
return fallback;
}
};
var betterAjvErrors = function betterAjvErrors(_ref) {
var errors = _ref.errors,
data = _ref.data,
schema = _ref.schema,
_ref$basePath = _ref.basePath,
basePath = _ref$basePath === void 0 ? '{base}' : _ref$basePath;
if (!Array.isArray(errors) || errors.length === 0) {
return [];
}
var definedErrors = filterSingleErrorPerProperty(errors);
return definedErrors.map(function (error) {
var path = pointerToDotNotation(basePath + error.instancePath);
var prop = getLastSegment(error.instancePath);
var defaultContext = {
errorType: error.keyword
};
var defaultMessage = (prop ? "property '" + prop + "'" : path) + " " + cleanAjvMessage(error.message);
var validationError;
switch (error.keyword) {
case 'additionalProperties':
{
var additionalProp = error.params.additionalProperty;
var suggestionPointer = error.schemaPath.replace('#', '').replace('/additionalProperties', '');
var _safeJsonPointer = safeJsonPointer({
object: schema,
pnter: suggestionPointer,
fallback: {
properties: {}
}
}),
properties = _safeJsonPointer.properties;
validationError = {
message: "'" + additionalProp + "' property is not expected to be here",
suggestion: getSuggestion({
value: additionalProp,
suggestions: Object.keys(properties != null ? properties : {}),
format: function format(suggestion) {
return "Did you mean property '" + suggestion + "'?";
}
}),
path: path,
context: defaultContext
};
break;
}
case 'enum':
{
var suggestions = error.params.allowedValues.map(function (value) {
return value.toString();
});
var _prop = getLastSegment(error.instancePath);
var value = safeJsonPointer({
object: data,
pnter: error.instancePath,
fallback: ''
});
validationError = {
message: "'" + _prop + "' property must be equal to one of the allowed values",
suggestion: getSuggestion({
value: value,
suggestions: suggestions
}),
path: path,
context: _extends({}, defaultContext, {
allowedValues: error.params.allowedValues
})
};
break;
}
case 'type':
{
var _prop2 = getLastSegment(error.instancePath);
var type = error.params.type;
validationError = {
message: "'" + _prop2 + "' property type must be " + type,
path: path,
context: defaultContext
};
break;
}
case 'required':
{
validationError = {
message: path + " must have required property '" + error.params.missingProperty + "'",
path: path,
context: defaultContext
};
break;
}
case 'const':
{
return {
message: "'" + prop + "' property must be equal to the allowed value",
path: path,
context: _extends({}, defaultContext, {
allowedValue: error.params.allowedValue
})
};
}
default:
return {
message: defaultMessage,
path: path,
context: defaultContext
};
} // Remove empty properties
var errorEntries = Object.entries(validationError);
for (var _i = 0, _errorEntries = errorEntries; _i < _errorEntries.length; _i++) {
var _errorEntries$_i = _errorEntries[_i],
key = _errorEntries$_i[0],
_value = _errorEntries$_i[1];
if (_value === null || _value === undefined || _value === '') {
delete validationError[key];
}
}
return validationError;
});
};
export { betterAjvErrors };
//# sourceMappingURL=better-ajv-errors.esm.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,5 @@
import { DefinedError } from 'ajv';
export declare const AJV_ERROR_KEYWORD_WEIGHT_MAP: Partial<Record<DefinedError['keyword'], number>>;
export declare const QUOTES_REGEX: RegExp;
export declare const NOT_REGEX: RegExp;
export declare const SLASH_REGEX: RegExp;

View file

@ -0,0 +1,11 @@
import { ErrorObject } from 'ajv';
import type { JSONSchema6 } from 'json-schema';
import { ValidationError } from './types/ValidationError';
export interface BetterAjvErrorsOptions {
errors: ErrorObject[] | null | undefined;
data: any;
schema: JSONSchema6;
basePath?: string;
}
export declare const betterAjvErrors: ({ errors, data, schema, basePath, }: BetterAjvErrorsOptions) => ValidationError[];
export { ValidationError };

View file

@ -0,0 +1,8 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./better-ajv-errors.cjs.production.min.js')
} else {
module.exports = require('./better-ajv-errors.cjs.development.js')
}

View file

@ -0,0 +1,2 @@
import { DefinedError } from 'ajv';
export declare const filterSingleErrorPerProperty: (errors: DefinedError[]) => DefinedError[];

View file

@ -0,0 +1,5 @@
export declare const getSuggestion: ({ value, suggestions, format, }: {
value: string | null;
suggestions: string[];
format?: ((suggestion: string) => string) | undefined;
}) => string;

View file

@ -0,0 +1,8 @@
export declare const pointerToDotNotation: (pointer: string) => string;
export declare const cleanAjvMessage: (message: string) => string;
export declare const getLastSegment: (path: string) => string;
export declare const safeJsonPointer: <T>({ object, pnter, fallback }: {
object: any;
pnter: string;
fallback: T;
}) => T;

View file

@ -0,0 +1,10 @@
import { DefinedError } from 'ajv';
export interface ValidationError {
message: string;
path: string;
suggestion?: string;
context: {
errorType: DefinedError['keyword'];
[additionalContext: string]: unknown;
};
}