diff options
Diffstat (limited to 'code/app/src/help')
| -rw-r--r-- | code/app/src/help/colors.ts | 47 | ||||
| -rw-r--r-- | code/app/src/help/index.ts | 466 | ||||
| -rw-r--r-- | code/app/src/help/logger.ts | 86 | ||||
| -rw-r--r-- | code/app/src/help/persistent-store.ts | 102 |
4 files changed, 701 insertions, 0 deletions
diff --git a/code/app/src/help/colors.ts b/code/app/src/help/colors.ts new file mode 100644 index 0000000..34c7992 --- /dev/null +++ b/code/app/src/help/colors.ts @@ -0,0 +1,47 @@ +export function generate_random_hex_color(skip_contrast_check = false) { + let hex = __generate_random_hex_color(); + if (skip_contrast_check) return hex; + while ((__calculate_contrast_ratio("#ffffff", hex) < 4.5) || (__calculate_contrast_ratio("#000000", hex) < 4.5)) { + hex = __generate_random_hex_color(); + } + + return hex; +} + +// Largely copied from chroma js api +function __generate_random_hex_color(): string { + let code = "#"; + for (let i = 0; i < 6; i++) { + code += "0123456789abcdef".charAt(Math.floor(Math.random() * 16)); + } + return code; +} + +function __calculate_contrast_ratio(hex1: string, hex2: string): number { + const rgb1 = __hex_to_rgb(hex1); + const rgb2 = __hex_to_rgb(hex2); + const l1 = __get_luminance(rgb1[0], rgb1[1], rgb1[2]); + const l2 = __get_luminance(rgb2[0], rgb2[1], rgb2[2]); + const result = l1 > l2 ? (l1 + 0.05) / (l2 + 0.05) : (l2 + 0.05) / (l1 + 0.05); + return result; +} + +function __hex_to_rgb(hex: string): number[] { + if (!hex.match(/^#([A-Fa-f0-9]{6})$/)) return []; + if (hex[0] === "#") hex = hex.substring(1, hex.length); + return [parseInt(hex.substring(0, 2), 16), parseInt(hex.substring(2, 4), 16), parseInt(hex.substring(4, 6), 16)]; +} + +function __get_luminance(r: any, g: any, b: any) { + // relative luminance + // see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + r = __luminance_x(r); + g = __luminance_x(g); + b = __luminance_x(b); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +function __luminance_x(x: any) { + x /= 255; + return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); +} diff --git a/code/app/src/help/index.ts b/code/app/src/help/index.ts new file mode 100644 index 0000000..a69228e --- /dev/null +++ b/code/app/src/help/index.ts @@ -0,0 +1,466 @@ +import {browser} from "$app/environment"; +import type {WorkEntry} from "$models/work/WorkEntry"; +import {log_info} from "./logger"; +import {Temporal} from "temporal-polyfill"; + +export const EMAIL_REGEX = new RegExp(/^([a-z0-9]+(?:([._\-])[a-z0-9]+)*@(?:[a-z0-9]+(?:(-)[a-z0-9]+)?\.)+[a-z0-9](?:[a-z0-9]*[a-z0-9])?)$/i); +export const URL_REGEX = new RegExp(/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-.][a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/gm); +export const GUID_REGEX = new RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i); +export const NORWEGIAN_PHONE_NUMBER_REGEX = new RegExp(/(0047|\+47|47)?\d{8,12}/); + +export function get_default_sorted(unsorted: Array<WorkEntry>): Array<WorkEntry> { + if (unsorted.length < 1) return unsorted; + const byStart = unsorted.sort((a, b) => { + return Temporal.Instant.compare(Temporal.Instant.from(b.start), Temporal.Instant.from(a.start)); + }); + + return byStart.sort((a, b) => { + return Temporal.Instant.compare(Temporal.Instant.from(b.stop), Temporal.Instant.from(a.stop)); + }); +} + +export function get_element_by_pw_key(key: string): HTMLElement | null { + return document.querySelector("[pw-key='" + key + "']"); +} + +export function get_pw_key_selector(key: string): string { + return "[pw-key='" + key + "']"; +} + +export function is_email(value: string): boolean { + return EMAIL_REGEX.test(String(value).toLowerCase()); +} + +export function is_url(value: string): boolean { + return URL_REGEX.test(String(value).toLowerCase()); +} + +export function is_norwegian_phone_number(value: string): boolean { + if (value.length < 8 || value.length > 12) { + return false; + } + return NORWEGIAN_PHONE_NUMBER_REGEX.test(String(value)); +} + +export function is_guid(value: string): boolean { + if (!value) { + return false; + } + if (value[0] === "{") { + value = value.substring(1, value.length - 1); + } + return GUID_REGEX.test(value); +} + +export function is_empty_object(obj: object): boolean { + return obj !== void 0 && Object.keys(obj).length > 0; +} + +export function merge_obj_arr<T>(a: Array<T>, b: Array<T>, props: Array<string>): Array<T> { + let start = 0; + let merge = []; + + while (start < a.length) { + + if (a[start] === b[start]) { + //pushing the merged objects into array + merge.push({...a[start], ...b[start]}); + } + //incrementing start value + start = start + 1; + } + return merge; +} + +export function set_favicon(url: string) { + // Find the current favicon element + const favicon = document.querySelector("link[rel=\"icon\"]") as HTMLLinkElement; + if (favicon) { + // Update the new link + favicon.href = url; + } else { + // Create new `link` + const link = document.createElement("link"); + link.rel = "icon"; + link.href = url; + + // Append to the `head` element + document.head.appendChild(link); + } +} + +export function no_type_check(x: any) { + return x; +} + +export function capitalise(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); +} + +export function set_emoji_favicon(emoji: string) { + // Create a canvas element + const canvas = document.createElement("canvas"); + canvas.height = 64; + canvas.width = 64; + + // Get the canvas context + const context = canvas.getContext("2d") as CanvasRenderingContext2D; + context.font = "64px serif"; + context.fillText(emoji, 0, 64); + + // Get the custom URL + const url = canvas.toDataURL(); + + // Update the favicon + set_favicon(url); +} + + +// https://stackoverflow.com/a/48400665/11961742 +export function seconds_to_hour_minute_string(seconds: number, hourChar = "h", minuteChar = "m") { + const hours = Math.floor(seconds / (60 * 60)); + seconds -= hours * (60 * 60); + const minutes = Math.floor(seconds / 60); + return hours + "h" + minutes + "m"; +} + +export function seconds_to_hour_minute(seconds: number) { + const hours = Math.floor(seconds / (60 * 60)); + seconds -= hours * (60 * 60); + const minutes = Math.floor(seconds / 60); + return {hours, minutes}; +} + +export function get_query_string(params: any = {}): string { + const map = Object.keys(params).reduce((arr: Array<string>, key: string) => { + if (params[key] !== undefined) { + return arr.concat(`${key}=${encodeURIComponent(params[key])}`); + } + return arr; + }, [] as any); + + if (map.length) { + return `?${map.join("&")}`; + } + + return ""; +} + +export function make_url(url: string, params: object): string { + return `${url}${get_query_string(params)}`; +} + +export function noop() { +} + +export async function run_async(functionToRun: Function): Promise<any> { + return new Promise((greatSuccess, graveFailure) => { + try { + greatSuccess(functionToRun()); + } catch (exception) { + graveFailure(exception); + } + }); +} + +// https://stackoverflow.com/a/45215694/11961742 +export function get_selected_options(domElement: HTMLSelectElement): Array<string> { + const ret = []; + + // fast but not universally supported + if (domElement.selectedOptions !== undefined) { + for (let i = 0; i < domElement.selectedOptions.length; i++) { + ret.push(domElement.selectedOptions[i].value); + } + + // compatible, but can be painfully slow + } else { + for (let i = 0; i < domElement.options.length; i++) { + if (domElement.options[i].selected) { + ret.push(domElement.options[i].value); + } + } + } + return ret; +} + +export function random_string(length: number): string { + if (!length) { + throw new Error("length is undefined"); + } + let result = ""; + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +interface CreateElementOptions { + name: string, + properties?: object, + children?: Array<HTMLElement | Function | Node> +} + +export function create_element_from_object(elementOptions: CreateElementOptions): HTMLElement { + return create_element(elementOptions.name, elementOptions.properties, elementOptions.children); +} + +export function create_element(name: string, properties?: object, children?: Array<HTMLElement | any>): HTMLElement { + if (!name || name.length < 1) { + throw new Error("name is required"); + } + const node = document.createElement(name); + if (properties) { + for (const [key, value] of Object.entries(properties)) { + // @ts-ignore + node[key] = value; + } + } + + if (children && children.length > 0) { + let actualChildren = children; + if (typeof children === "function") { + // @ts-ignore + actualChildren = children(); + } + for (const child of actualChildren) { + node.appendChild(child as Node); + } + } + return node; +} + +export function get_element_position(element: HTMLElement | any) { + if (!element) return {x: 0, y: 0}; + let x = 0; + let y = 0; + while (true) { + x += element.offsetLeft; + y += element.offsetTop; + if (element.offsetParent === null) { + break; + } + element = element.offsetParent; + } + return {x, y}; +} + +export function restrict_input_to_numbers(element: HTMLElement, specials: Array<string> = [], mergeSpecialsWithDefaults: boolean = false): void { + if (element) { + element.addEventListener("keydown", (e) => { + const defaultSpecials = ["Backspace", "ArrowLeft", "ArrowRight", "Tab"]; + let keys = specials.length > 0 ? specials : defaultSpecials; + if (mergeSpecialsWithDefaults && specials) { + keys = [...specials, ...defaultSpecials]; + } + if (keys.indexOf(e.key) !== -1) { + return; + } + if (isNaN(parseInt(e.key))) { + e.preventDefault(); + } + }); + } +} + +export function element_has_focus(element: HTMLElement): boolean { + return element === document.activeElement; +} + +export function move_focus(element: HTMLElement): void { + if (!element) { + element = document.getElementsByTagName("body")[0]; + } + element.focus(); + // @ts-ignore + if (!element_has_focus(element)) { + element.setAttribute("tabindex", "-1"); + element.focus(); + } +} + +export function get_url_parameter(name: string): string { + // @ts-ignore + return new RegExp("[?&]" + name + "=([^&#]*)")?.exec(window.location.href)[1]; +} + +export function update_url_parameter(param: string, newVal: string): void { + let newAdditionalURL = ""; + let tempArray = location.href.split("?"); + const baseURL = tempArray[0]; + const additionalURL = tempArray[1]; + let temp = ""; + if (additionalURL) { + tempArray = additionalURL.split("&"); + for (let i = 0; i < tempArray.length; i++) { + if (tempArray[i].split("=")[0] !== param) { + newAdditionalURL += temp + tempArray[i]; + temp = "&"; + } + } + } + const rows_txt = temp + "" + param + "=" + newVal; + const newUrl = baseURL + "?" + newAdditionalURL + rows_txt; + window.history.replaceState("", "", newUrl); +} + + +export function get_style_string(rules: CSSRuleList) { + let styleString = ""; + for (const [key, value] of Object.entries(rules)) { + styleString += key + ":" + value + ";"; + } + return styleString; +} + +export function parse_iso_local(s: string) { + const b = s.split(/\D/); + //@ts-ignore + return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]); +} + +export function resolve_references(json: any) { + if (!json) return; + if (typeof json === "string") { + json = JSON.parse(json ?? "{}"); + } + const byid = {}, refs = []; + json = function recurse(obj, prop, parent) { + if (typeof obj !== "object" || !obj) { + return obj; + } + if (Object.prototype.toString.call(obj) === "[object Array]") { + for (let i = 0; i < obj.length; i++) { + if (typeof obj[i] !== "object" || !obj[i]) { + continue; + } else if ("$ref" in obj[i]) { + // @ts-ignore + obj[i] = recurse(obj[i], i, obj); + } else { + obj[i] = recurse(obj[i], prop, obj); + } + } + return obj; + } + if ("$ref" in obj) { + let ref = obj.$ref; + if (ref in byid) { + // @ts-ignore + return byid[ref]; + } + refs.push([parent, prop, ref]); + return; + } else if ("$id" in obj) { + let id = obj.$id; + delete obj.$id; + if ("$values" in obj) { + obj = obj.$values.map(recurse); + } else { + for (let prop2 in obj) { + // @ts-ignore + obj[prop2] = recurse(obj[prop2], prop2, obj); + } + } + // @ts-ignore + byid[id] = obj; + } + return obj; + }(json); + for (let i = 0; i < refs.length; i++) { + let ref = refs[i]; + // @ts-ignore + ref[0][ref[1]] = byid[ref[2]]; + } + return json; +} + +export function get_random_int(min: number, max: number): number { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export function to_readable_bytes(bytes: number): string { + const s = ["bytes", "kB", "MB", "GB", "TB", "PB"]; + const e = Math.floor(Math.log(bytes) / Math.log(1024)); + return (bytes / Math.pow(1024, e)).toFixed(2) + " " + s[e]; +} + +export function can_use_dom(): boolean { + return !!(typeof window !== "undefined" && window.document && window.document.createElement); +} + +export function session_storage_remove_regex(regex: RegExp): void { + if (!browser) { + log_info("sessionStorage is not available in non-browser contexts"); + return; + } + let n = sessionStorage.length; + while (n--) { + const key = sessionStorage.key(n); + if (key && regex.test(key)) { + sessionStorage.removeItem(key); + } + } +} + +export function local_storage_remove_regex(regex: RegExp): void { + if (!browser) { + log_info("sessionStorage is not available in non-browser contexts"); + return; + } + let n = localStorage.length; + while (n--) { + const key = localStorage.key(n); + if (key && regex.test(key)) { + localStorage.removeItem(key); + } + } +} + +export function session_storage_set_json(key: string, value: object): void { + if (!browser) { + console.warn("sessionStorage is not available in non-browser contexts"); + return; + } + sessionStorage.setItem(key, JSON.stringify(value)); +} + +export function session_storage_get_json(key: string): object { + if (!browser) { + console.warn("sessionStorage is not available in non-browser contexts"); + return {}; + } + return JSON.parse(sessionStorage.getItem(key) ?? "{}"); +} + +export function local_storage_set_json(key: string, value: object): void { + if (!browser) { + console.warn("sessionStorage is not available in non-browser contexts"); + return; + } + localStorage.setItem(key, JSON.stringify(value)); +} + +export function local_storage_get_json(key: string): object { + if (!browser) { + console.warn("sessionStorage is not available in non-browser contexts"); + return {}; + } + return JSON.parse(localStorage.getItem(key) ?? "{}"); +} + +export function get_hash_code(value: string): number | undefined { + let hash = 0; + if (value.length === 0) { + return; + } + for (let i = 0; i < value.length; i++) { + const char = value.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash |= 0; + } + return hash; +} diff --git a/code/app/src/help/logger.ts b/code/app/src/help/logger.ts new file mode 100644 index 0000000..a5b450b --- /dev/null +++ b/code/app/src/help/logger.ts @@ -0,0 +1,86 @@ +import {browser, dev} from "$app/environment"; +import {StorageKeys} from "$configuration"; +import pino from "pino"; + +const pinoConfig = dev ? { + transport: { + target: "pino-pretty", + }, +} : {}; + +const pinoLogger = pino(pinoConfig); + +function browser_log_level(): number { + if (browser) return LogLevel.to_number(sessionStorage.getItem(StorageKeys.logLevel), LogLevel.INFO); + throw new Error("Called browser api in server"); +} + +function server_log_level(): number { + if (!browser) return LogLevel.to_number(import.meta.env.VITE_LOG_LEVEL, LogLevel.ERROR); + throw new Error("Called server api in browser"); +} + +export const LogLevel = { + DEBUG: 0, + INFO: 1, + ERROR: 2, + SILENT: 3, + to_string(levelInt: number): string { + switch (levelInt) { + case 0: + return "DEBUG"; + case 1: + return "INFO"; + case 2: + return "ERROR"; + case 3: + return "SILENT"; + default: + throw new Error("Log level int is unknown"); + } + }, + to_number(levelString?: string | null, fallback?: number): number { + if (!levelString && fallback) return fallback; + else if (!levelString && !fallback) throw new Error("levelString was empty, and no fallback was specified"); + switch (levelString?.toUpperCase()) { + case "DEBUG": + return 0; + case "INFO": + return 1; + case "ERROR": + return 2; + case "SILENT": + return 3; + default: + if (!fallback) throw new Error("Log level string is unknown"); + else return fallback; + } + }, +}; + +export function log_debug(message: string, ...additional: any[]): void { + if (browser && browser_log_level() <= LogLevel.DEBUG) { + pinoLogger.debug(message, additional); + } + if (!browser && server_log_level() <= LogLevel.DEBUG) { + pinoLogger.debug(message, additional); + } +} + +export function log_info(message: string, ...additional: any[]): void { + if (browser && browser_log_level() <= LogLevel.INFO) { + pinoLogger.info(message, additional); + } + if (!browser && server_log_level() <= LogLevel.INFO) { + pinoLogger.info(message, additional); + } +} + +export function log_error(message: any, ...additional: any[]): void { + if (browser && browser_log_level() <= LogLevel.ERROR) { + pinoLogger.error(message, additional); + } + if (!browser && server_log_level() <= LogLevel.ERROR) { + pinoLogger.error(message, additional); + } +}
\ No newline at end of file diff --git a/code/app/src/help/persistent-store.ts b/code/app/src/help/persistent-store.ts new file mode 100644 index 0000000..f2c14c9 --- /dev/null +++ b/code/app/src/help/persistent-store.ts @@ -0,0 +1,102 @@ +import {writable as _writable, readable as _readable} from "svelte/store"; +import type {Writable, Readable, StartStopNotifier} from "svelte/store"; + +enum StoreType { + SESSION = 0, + LOCAL = 1 +} + +interface StoreOptions { + store?: StoreType; +} + +const default_store_options = { + store: StoreType.SESSION, +} as StoreOptions; + +interface WritableStore<T> { + name: string, + initialState: T, + options?: StoreOptions +} + +interface ReadableStore<T> { + name: string, + initialState: T, + callback: StartStopNotifier<any>, + options?: StoreOptions +} + +function get_store(type: StoreType): Storage { + switch (type) { + case StoreType.SESSION: + return window.sessionStorage; + case StoreType.LOCAL: + return window.localStorage; + } +} + +function prepared_store_value(value: any): string { + try { + return JSON.stringify(value); + } catch (e) { + console.error(e); + return "__INVALID__"; + } +} + +function get_store_value<T>(options: WritableStore<T> | ReadableStore<T>): any { + try { + const storage = get_store(options.options.store); + const value = storage.getItem(options.name); + if (!value) return false; + return JSON.parse(value); + } catch (e) { + console.error(e); + return {__INVALID__: true}; + } +} + +function hydrate<T>(store: Writable<T>, options: WritableStore<T> | ReadableStore<T>): void { + const value = get_store_value<T>(options); + if (value && store.set) store.set(value); +} + +function subscribe<T>(store: Writable<T> | Readable<T>, options: WritableStore<T> | ReadableStore<T>): void { + const storage = get_store(options.options.store); + if (!store.subscribe) return; + store.subscribe((state: any) => { + storage.setItem(options.name, prepared_store_value(state)); + }); +} + +function writable_persistent<T>(options: WritableStore<T>): Writable<T> { + if (options.options === undefined) options.options = default_store_options; + console.log("Creating writable store with options: ", options); + const store = _writable<T>(options.initialState); + hydrate(store, options); + subscribe(store, options); + return store; +} + +function readable_persistent<T>(options: ReadableStore<T>): Readable<T> { + if (options.options === undefined) options.options = default_store_options; + console.log("Creating readable store with options: ", options); + const store = _readable<T>(options.initialState, options.callback); + // hydrate(store, options); + subscribe(store, options); + return store; +} + +export { + writable_persistent, + readable_persistent, + StoreType, +}; + +export type { + WritableStore, + ReadableStore, + StoreOptions, +}; + |
