diff options
Diffstat (limited to 'code/app/src/lib')
81 files changed, 0 insertions, 3803 deletions
diff --git a/code/app/src/lib/api/_fetch.ts b/code/app/src/lib/api/_fetch.ts deleted file mode 100644 index c29d262..0000000 --- a/code/app/src/lib/api/_fetch.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Temporal } from "temporal-polyfill"; -import { clear_session_data } from "$lib/session"; -import { redirect } from "@sveltejs/kit"; -import { browser } from "$app/environment"; -import { goto } from "$app/navigation"; -import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in"; -import { log_error } from "$lib/logger"; - -export async function http_post_async(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> { - const init = make_request_init("post", body, abort_signal); - const response = await internal_fetch_async({ url, init, timeout }); - if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401"); - return response; -} - -export async function http_get_async(url: string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> { - const init = make_request_init("get", undefined, abort_signal); - const response = await internal_fetch_async({ url, init, timeout }); - if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401"); - return response; -} - -export async function http_delete_async(url: string, body?: object | string, timeout = -1, skip_401_check = false, abort_signal?: AbortSignal): Promise<Response> { - const init = make_request_init("delete", body, abort_signal); - const response = await internal_fetch_async({ url, init, timeout }); - if (!skip_401_check && await redirect_if_401_async(response)) throw new Error("Server returned 401"); - return response; -} - -async function internal_fetch_async(request: InternalFetchRequest): Promise<Response> { - if (!request.init) throw new Error("request.init is required"); - const fetch_request = new Request(request.url, request.init); - let response: any; - - try { - if (request.timeout && request.timeout > 500) { - response = await Promise.race([ - fetch(fetch_request), - new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), request.timeout)), - ]); - } else { - response = await fetch(fetch_request); - } - } catch (error: any) { - log_error(error); - if (error.message === "Timeout") { - console.error("Request timed out"); - } else if (error.message === "Network request failed") { - console.error("No internet connection"); - } else { - throw error; - } - } - - return response; -} - -async function redirect_if_401_async(response: Response): Promise<boolean> { - if (response.status === 401) { - const redirectUrl = `/sign-in?${signInPageMessageQueryKey}=${SignInPageMessage.LOGGED_OUT}`; - clear_session_data(); - if (browser) { - await goto(redirectUrl); - } else { - throw redirect(307, redirectUrl); - } - } - return false; -} - -function make_request_init(method: string, body?: any, signal?: AbortSignal): RequestInit { - const init = { - method, - credentials: "include", - signal, - headers: { - "X-TimeZone": Temporal.Now.timeZone().id, - }, - } as RequestInit; - - if (body) { - init.body = JSON.stringify(body); - init.headers["Content-Type"] = "application/json;charset=UTF-8"; - } - - return init; -} - - -export type InternalFetchRequest = { - url: string, - init: RequestInit, - timeout?: number - retry_count?: number, -}
\ No newline at end of file diff --git a/code/app/src/lib/api/account/index.ts b/code/app/src/lib/api/account/index.ts deleted file mode 100644 index 6dbcdc8..0000000 --- a/code/app/src/lib/api/account/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {api_base} from "$lib/configuration"; -import {http_delete_async, http_get_async, http_post_async} from "../_fetch"; - -export const http_account = { - login_async(payload: LoginPayload): Promise<Response> { - return http_post_async(api_base("_/account/login"), payload); - }, - logout_async(): Promise<Response> { - return http_get_async(api_base("_/account/logout")); - }, - delete_account_async(): Promise<Response> { - return http_delete_async(api_base("_/account/delete")); - }, - update_profile_async(payload: UpdateProfilePayload): Promise<Response> { - return http_post_async(api_base("_/account/update"), payload); - }, - create_account_async(payload: CreateAccountPayload): Promise<Response> { - return http_post_async(api_base("_/account/create"), payload); - }, - get_profile_async(suppress_401: boolean): Promise<Response> { - return http_get_async(api_base("_/account"), 0, suppress_401); - }, -}; - -export interface CreateAccountPayload { - username: string, - password: string -} - -export interface LoginPayload { - username: string, - password: string, - persist: boolean -} - -export interface UpdateProfilePayload { - username?: string, - password?: string, -} diff --git a/code/app/src/lib/api/api-tokens/index.ts b/code/app/src/lib/api/api-tokens/index.ts deleted file mode 100644 index 77bfd91..0000000 --- a/code/app/src/lib/api/api-tokens/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {http_delete_async, http_get_async, http_post_async} from "src/lib/api/_fetch"; -import {api_base} from "src/lib/configuration"; -import type {Temporal} from "temporal-polyfill"; - -export const http_api_tokens = { - create_token_async(payload: CreateTokenPayload): Promise<Response> { - return http_post_async(api_base("v1/api-tokens/create"), payload); - }, - delete_token_async(id: string): Promise<Response> { - return http_delete_async(api_base("v1/api-tokens/delete?id=" + id)); - }, - get_tokens_async(): Promise<Response> { - return http_get_async(api_base("v1/api-tokens")) - }, -}; - -export type CreateTokenPayload = { - expiryDate: Temporal.PlainDateTime, - allowRead: boolean, - allowCreate: boolean, - allowUpdate: boolean, - allowDelete: boolean -}
\ No newline at end of file diff --git a/code/app/src/lib/api/projects/index.ts b/code/app/src/lib/api/projects/index.ts deleted file mode 100644 index ea49631..0000000 --- a/code/app/src/lib/api/projects/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { api_base } from "$lib/configuration"; -import { http_post_async } from "../_fetch"; - -export const http_projects = { - create_async(payload: CreateProjectPayload): Promise<Response> { - return http_post_async(api_base("projects/create", true), payload); - } -}; - -export type CreateProjectPayload = { - name: "" -}
\ No newline at end of file diff --git a/code/app/src/lib/colors.ts b/code/app/src/lib/colors.ts deleted file mode 100644 index 34c7992..0000000 --- a/code/app/src/lib/colors.ts +++ /dev/null @@ -1,47 +0,0 @@ -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/lib/components/alert.svelte b/code/app/src/lib/components/alert.svelte deleted file mode 100644 index fd57105..0000000 --- a/code/app/src/lib/components/alert.svelte +++ /dev/null @@ -1,268 +0,0 @@ -<script lang="ts"> - import { random_string } from "$lib/helpers"; - import { createEventDispatcher } from "svelte"; - import { onMount } from "svelte"; - import pwKey from "$actions/pwKey"; - import { Temporal } from "temporal-polyfill"; - import { ExclamationTriangleIcon, CheckCircleIcon, InformationCircleIcon, XCircleIcon, XMarkIcon } from "./icons"; - - const dispatch = createEventDispatcher(); - const noCooldownSetting = "no-cooldown"; - - let iconComponent: any; - let colorClassPart = ""; - - /** - * An optional id for this alert, a default is set if not specified. - * This value is necessary for closeable cooldown to work. - */ - // if no unique id is supplied, cooldown will not work between page loads. - // Therefore we are disabling it with noCooldownSetting in the fallback id. - export let id = "alert--" + noCooldownSetting + "--" + random_string(4); - /** - * The title to communicate, value is optional - */ - export let title = ""; - /** - * The message to communicate, value is optional - */ - export let message = ""; - /** - * Changes the alerts color and icon. - */ - export let type: "info" | "success" | "warning" | "error" = "info"; - /** - * If true the alert can be removed from the DOM by clicking on a X icon on the upper right hand courner - */ - export let closeable = false; - /** - * The amount of seconds that should go by before this alert is shown again, only works when a unique id is set. - * Set to ~ if it should only be shown once per client (State stored in localestorage). - **/ - export let closeableCooldown = "-1"; - /** - * The text that is displayed on the right link - */ - export let rightLinkText = ""; - /** - * An array of list items displayed under the message or title - */ - export let listItems: Array<string> = []; - /** - * An array of {id:string;text:string;color?:string}, where id is dispatched back as an svelte event with this syntax act$id (ex: on:actcancel). - * Text is the button text - * Color is the optional tailwind color to used, the value is used in classes like bg-$color-50. - */ - export let actions: Array<{ id: string; text: string; color?: string }> = []; - /** - * This value is set on a plain anchor tag without any svelte routing, - * listen to the on:rightLinkClick if you want to intercept the click without navigating - */ - export let rightLinkHref = "javascript:void(0)"; - $: cooldownEnabled = - id.indexOf(noCooldownSetting) === -1 && closeable && (closeableCooldown === "~" || parseInt(closeableCooldown) > 0); - /** - * Sets this alerts visibility state, when this is false it is removed from the dom using an {#if} block. - */ - export let visible = closeableCooldown === "~" || parseInt(closeableCooldown) > 0 ? false : true; - - export let _pwKey: string | undefined = undefined; - - const cooldownStorageKey = "lastseen--" + id; - - $: switch (type) { - case "info": { - colorClassPart = "blue"; - iconComponent = InformationCircleIcon; - break; - } - case "warning": { - colorClassPart = "yellow"; - iconComponent = ExclamationTriangleIcon; - break; - } - case "error": { - colorClassPart = "red"; - iconComponent = XCircleIcon; - break; - } - case "success": { - colorClassPart = "green"; - iconComponent = CheckCircleIcon; - break; - } - } - - function close() { - visible = false; - if (cooldownEnabled) { - console.log("Cooldown enabled for " + id + ", " + closeableCooldown === "~" ? "with an endless cooldown" : ""); - localStorage.setItem(cooldownStorageKey, String(Temporal.Now.instant().epochSeconds)); - } - } - - function rightLinkClicked() { - dispatch("rightLinkCliked"); - } - - function actionClicked(name: string) { - dispatch("act" + name); - } - - // Manages the state of the alert if cooldown is enabled - function run_cooldown() { - if (!cooldownEnabled) { - console.log("Alert cooldown is not enabled for " + id); - return; - } - if (!localStorage.getItem(cooldownStorageKey)) { - console.log("Alert " + id + " has not been seen yet, displaying"); - visible = true; - return; - } - // if (!visible) { - // console.log( - // "Alert " + id + " is not visible, stopping cooldown change" - // ); - // return; - // } - if (closeableCooldown === "~") { - console.log("Alert " + id + " has an infinite cooldown, hiding"); - visible = false; - return; - } - - const lastSeen = Temporal.Instant.fromEpochSeconds(parseInt(localStorage.getItem(cooldownStorageKey) ?? "-1")); - if (Temporal.Instant.compare(Temporal.Now.instant(), lastSeen.add({ seconds: parseInt(closeableCooldown) })) === 1) { - console.log( - "Alert " + - id + - " has a cooldown of " + - closeableCooldown + - " and was last seen " + - lastSeen.toLocaleString() + - " making it due for a showing" - ); - visible = true; - } else { - visible = false; - } - } - - onMount(() => { - if (cooldownEnabled) { - run_cooldown(); - } - - if (closeable && closeableCooldown && id.indexOf(noCooldownSetting) !== -1) { - // TODO: This prints twice before shutting up as it should, in this example look at the only alert with closeableCooldown in alertsbook. - // Looks like svelte mounts three times and that my id is only set on the third. Not sure it does at all after logging the id onMount. - console.error("Alert cooldown does not work without specifying a unique id, related id: " + id); - } - }); -</script> - -{#if visible} - <div class="rounded-md bg-{colorClassPart}-50 p-4 {$$restProps.class ?? ''}" use:pwKey={_pwKey}> - <div class="flex"> - <div class="flex-shrink-0"> - <svelte:component this={iconComponent} class="text-{colorClassPart}-400" /> - </div> - <div class="ml-3 text-sm w-full"> - {#if !rightLinkText} - {#if title} - <h3 class="font-medium text-{colorClassPart}-800"> - {title} - </h3> - {/if} - {#if message} - <div class="{title ? 'mt-2' : ''} text-{colorClassPart}-700 justify-start"> - <p> - {@html message} - </p> - </div> - {/if} - {#if listItems?.length ?? 0} - <ul class="list-disc space-y-1 pl-5 text-{colorClassPart}-700"> - {#each listItems as listItem} - <li>{listItem}</li> - {/each} - </ul> - {/if} - {:else} - <div class="flex-1 md:flex md:justify-between"> - <div> - {#if title} - <h3 class="font-medium text-{colorClassPart}-800"> - {title} - </h3> - {/if} - {#if message} - <div class="{title ? 'mt-2' : ''} text-{colorClassPart}-700 justify-start"> - <p> - {@html message} - </p> - </div> - {/if} - {#if listItems?.length ?? 0} - <ul class="list-disc space-y-1 pl-5 text-{colorClassPart}-700"> - {#each listItems as listItem} - <li>{listItem}</li> - {/each} - </ul> - {/if} - </div> - <p class="mt-3 text-sm md:mt-0 md:ml-6 flex items-end"> - <a - href={rightLinkHref} - on:click={() => rightLinkClicked()} - class="whitespace-nowrap font-medium text-{colorClassPart}-700 hover:text-{colorClassPart}-600" - > - {rightLinkText} - <span aria-hidden="true"> →</span> - </a> - </p> - </div> - {/if} - {#if actions?.length ?? 0} - <div class="ml-2 mt-4"> - <div class="-mx-2 -my-1.5 flex gap-1"> - {#each actions as action} - {@const color = action?.color ?? colorClassPart} - <button - type="button" - on:click={() => actionClicked(action.id)} - class="rounded-md - bg-{color}-50 - px-2 py-1.5 text-sm font-medium - text-{color}-800 - hover:bg-{color}-100 - focus:outline-none focus:ring-2 - focus:ring-{color}-600 - focus:ring-offset-2 - focus:ring-offset-{color}-50" - > - {action.text} - </button> - {/each} - </div> - </div> - {/if} - </div> - {#if closeable} - <div class="ml-auto pl-3"> - <div class="-mx-1.5 -my-1.5"> - <button - type="button" - on:click={() => close()} - class="inline-flex rounded-md bg-{colorClassPart}-50 p-1.5 text-{colorClassPart}-500 hover:bg-{colorClassPart}-100 focus:outline-none focus:ring-2 focus:ring-{colorClassPart}-600 focus:ring-offset-2 focus:ring-offset-{colorClassPart}-50" - > - <span class="sr-only">Dismiss</span> - <XMarkIcon /> - </button> - </div> - </div> - {/if} - </div> - </div> -{/if} diff --git a/code/app/src/lib/components/badge.svelte b/code/app/src/lib/components/badge.svelte deleted file mode 100644 index 6ec48d5..0000000 --- a/code/app/src/lib/components/badge.svelte +++ /dev/null @@ -1,76 +0,0 @@ -<script lang="ts"> - import { createEventDispatcher } from "svelte"; - - export let id: string | undefined = undefined; - export let type: "default" | "red" | "yellow" | "green" | "blue" | "tame" = "default"; - export let text: string; - export let size: "large" | "default" = "default"; - export let withDot: boolean = false; - export let removable: boolean = false; - export let uppercase: boolean = false; - export let tabindex: string | undefined = undefined; - - let colorName = "gray"; - let sizeClass = "rounded px-2 py-0.5 text-xs"; - let dotSizeClass = "mr-1.5 h-2 w-2"; - - const dispatch = createEventDispatcher(); - - function handle_remove(event) { - dispatch("remove", { event, id, text }); - } - - $: switch (type) { - case "red": - colorName = "red"; - break; - case "yellow": - colorName = "yellow"; - break; - case "blue": - colorName = "blue"; - break; - case "green": - colorName = "teal"; - break; - case "default": - case "tame": - default: - colorName = "gray"; - break; - } - - $: switch (size) { - case "large": - sizeClass = "rounded-md px-2.5 py-0.5 text-sm"; - dotSizeClass = "-ml-0.5 mr-1.5 h-2 w-2"; - break; - case "default": - default: - sizeClass = "rounded px-2 py-0.5 text-xs"; - dotSizeClass = "mr-1.5 h-2 w-2"; - break; - } -</script> - -<span class="inline-flex items-center font-medium {uppercase ? 'uppercase' : ''} bg-{colorName}-100 text-{colorName}-800 {sizeClass}" {id}> - {#if withDot} - <svg class="{dotSizeClass} text-{colorName}-400" fill="currentColor" viewBox="0 0 8 8"> - <circle cx="4" cy="4" r="3" /> - </svg> - {/if} - {text} - {#if removable} - <button - on:click={handle_remove} - tabindex={parseInt(tabindex)} - type="button" - class="ml-0.5 inline-flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full text-{colorName}-400 hover:bg-{colorName}-200 hover:text-{colorName}-500 focus:bg-{colorName}-500 focus:text-white focus:outline-none" - > - <span class="sr-only">Remove badge</span> - <svg class="h-2 w-2" stroke="currentColor" fill="none" viewBox="0 0 8 8"> - <path stroke-linecap="round" stroke-width="1.5" d="M1 1l6 6m0-6L1 7" /> - </svg> - </button> - {/if} -</span> diff --git a/code/app/src/lib/components/button.svelte b/code/app/src/lib/components/button.svelte deleted file mode 100644 index 49a9354..0000000 --- a/code/app/src/lib/components/button.svelte +++ /dev/null @@ -1,115 +0,0 @@ -<script context="module" lang="ts"> - export type ButtonKind = "primary" | "secondary" | "white" | "reset"; - export type ButtonSize = "sm" | "lg" | "md" | "xl"; -</script> - -<script lang="ts"> - import pwKey from "$actions/pwKey"; - import { SpinnerIcon } from "./icons"; - - export let kind = "primary" as ButtonKind; - export let size = "md" as ButtonSize; - export let type: "button" | "submit" | "reset" = "button"; - export let id: string | undefined = undefined; - export let tabindex: string | undefined = undefined; - export let style: string | undefined = undefined; - export let title: string | undefined = undefined; - export let disabled: boolean | null = false; - export let href: string | undefined = undefined; - export let text: string; - export let loading = false; - export let fullWidth = false; - export let _pwKey: string | undefined = undefined; - - let sizeClasses = ""; - let kindClasses = ""; - let spinnerTextClasses = ""; - let spinnerMarginClasses = ""; - - $: shared_props = { - type: type, - id: id || null, - title: title || null, - disabled: disabled || loading || null, - tabindex: tabindex || null, - style: style || null, - } as any; - - $: switch (size) { - case "sm": - sizeClasses = "px-2.5 py-1.5 text-xs"; - spinnerMarginClasses = "mr-2"; - break; - case "md": - sizeClasses = "px-3 py-2 text-sm"; - spinnerMarginClasses = "mr-2"; - break; - case "lg": - sizeClasses = "px-3 py-2 text-lg"; - spinnerMarginClasses = "mr-2"; - break; - case "xl": - sizeClasses = "px-6 py-3 text-xl"; - spinnerMarginClasses = "mr-2"; - break; - } - - $: switch (kind) { - case "secondary": - kindClasses = "border-transparent text-teal-800 bg-teal-100 hover:bg-teal-200 focus:ring-teal-500"; - spinnerTextClasses = "teal-800"; - break; - case "primary": - kindClasses = "border-transparent text-teal-900 bg-teal-300 hover:bg-teal-400 focus:ring-teal-500"; - spinnerTextClasses = "text-teal-900"; - break; - case "white": - kindClasses = "border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-gray-400"; - spinnerTextClasses = "text-gray-700"; - break; - case "reset": - kindClasses = "reset outline-none ring-0 focus:ring-0 focus-visible:ring-0"; - break; - } -</script> - -{#if href} - <a - use:pwKey={_pwKey} - {...shared_props} - {href} - class="{sizeClasses} {kindClasses} {loading ? 'disabled:' : ''} {$$restProps.class ?? ''} {fullWidth - ? 'w-full justify-center' - : ''} inline-flex items-center border font-medium rounded shadow-sm focus:outline-none focus:ring-2" - > - {#if loading} - <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses} /> - {/if} - {text} - </a> -{:else} - <button - use:pwKey={_pwKey} - {...shared_props} - on:click - class="btn {sizeClasses} {kindClasses} {$$restProps.class ?? ''} - {fullWidth - ? 'w-full justify-center' - : ''} inline-flex items-center border font-medium rounded shadow-sm focus:outline-none focus:ring-2" - > - {#if loading} - <SpinnerIcon class={spinnerTextClasses + " " + spinnerMarginClasses} /> - {/if} - {text} - </button> -{/if} - -<style> - .reset { - border: 0px; - outline: none; - } - .reset:focus { - outline: none; - } -</style> diff --git a/code/app/src/lib/components/checkbox.svelte b/code/app/src/lib/components/checkbox.svelte deleted file mode 100644 index 12ebedb..0000000 --- a/code/app/src/lib/components/checkbox.svelte +++ /dev/null @@ -1,29 +0,0 @@ -<script lang="ts"> - import pwKey from "$actions/pwKey"; - import { random_string } from "$lib/helpers"; - - export let label: string; - export let id: string | undefined = "input__" + random_string(4); - export let name: string | undefined = undefined; - export let disabled: boolean | null = null; - export let checked: boolean; - export let required: boolean | null = null; - export let _pwKey: string | undefined = undefined; -</script> - -<div class="flex items-center"> - <input - {name} - use:pwKey={_pwKey} - {disabled} - {id} - {required} - type="checkbox" - bind:checked - class="h-4 w-4 text-teal-600 focus:ring-teal-500 border-gray-300 rounded" - /> - <label for={id} class="ml-2 block text-sm text-gray-900"> - {@html required ? "<span class='text-red-500'>*</span>" : ""} - {label} - </label> -</div> diff --git a/code/app/src/lib/components/combobox.svelte b/code/app/src/lib/components/combobox.svelte deleted file mode 100644 index 4e7b1cd..0000000 --- a/code/app/src/lib/components/combobox.svelte +++ /dev/null @@ -1,450 +0,0 @@ -<script lang="ts" context="module"> - export type ComboboxOption = { - id: string; - name: string; - selected?: boolean; - }; -</script> - -<script lang="ts"> - import { CheckCircleIcon, ChevronUpDownIcon, XIcon } from "./icons"; - import { element_has_focus, random_string } from "$lib/helpers"; - import { go, highlight } from "fuzzysort"; - import Badge from "./badge.svelte"; - import Button from "./button.svelte"; - import LL from "$lib/i18n/i18n-svelte"; - - export let id = "combobox-" + random_string(3); - export let label: string | undefined = undefined; - export let errorText: string | undefined = undefined; - export let disabled: boolean | undefined = undefined; - export let required: boolean | undefined = undefined; - export let maxlength: number | undefined = undefined; - export let placeholder: string = $LL.combobox.search(); - export let options: Array<ComboboxOption> | undefined = []; - export let createable = false; - export let loading = false; - export let multiple = false; - export let noResultsText: string = $LL.combobox.noRecordsFound(); - export let on_create_async = async ({ name: string }) => {}; - - export const reset = () => methods.reset(); - export const select = (id: string) => methods.select_entry(id); - export const deselect = (id: string) => methods.deselect_entry(id); - - const INTERNAL_ID = "INTERNAL__" + id; - - let optionsListId = id + "--options"; - let searchInputNode; - let searchResults: Array<any> = []; - let searchValue = ""; - let showCreationHint = false; - let showDropdown = false; - let inputHasFocus = false; - let lastKeydownCode = ""; - let mouseIsOverDropdown = false; - let mouseIsOverComponent = false; - - $: ariaErrorDescribedBy = id + "__" + "error"; - $: colorName = errorText ? "red" : "teal"; - $: attributes = { - "aria-describedby": errorText ? ariaErrorDescribedBy : null, - "aria-invalid": errorText ? "true" : null, - disabled: disabled || null, - required: required || null, - maxlength: maxlength || null, - id: id || null, - placeholder: placeholder || null, - } as any; - $: hasSelection = options.some((c) => c.selected === true); - $: if (searchValue.trim()) { - showCreationHint = createable && options.every((c) => search.normalise_value(c.name) !== search.normalise_value(searchValue)); - } else { - showCreationHint = false; - options = methods.get_sorted_array(options); - } - - function on_select(event) { - const node = event.target.closest("[data-id]"); - if (!node) return; - methods.select_entry(node.dataset.id); - } - - const search = { - normalise_value(value: string): string { - if (!value) { - return ""; - } - return value.trim().toLowerCase(); - }, - do() { - const query = search.normalise_value(searchValue); - - if (!query.trim()) { - searchResults = []; - return; - } - - // @ts-ignore - searchResults = go(query, options, { - limit: 15, - allowTypo: true, - threshold: -10000, - key: "name", - }); - showDropdown = true; - }, - on_input_focus() { - showDropdown = true; - inputHasFocus = true; - }, - on_input_click() { - showDropdown = true; - inputHasFocus = true; - }, - on_input_focusout() { - inputHasFocus = false; - if (lastKeydownCode !== "Tab" && (mouseIsOverDropdown || lastKeydownCode === "ArrowDown")) { - return; - } - const selected = options.find((c) => c.selected === true); - if (selected && !multiple) { - searchValue = selected.name; - } - document.querySelector("#" + INTERNAL_ID + " ul li.focus")?.classList.remove("focus"); - showDropdown = false; - }, - on_input_wrapper_focus(event) { - if (event.code && event.code !== "Space" && event.code !== "Enter") return; - if (!element_has_focus(searchInputNode)) searchInputNode.focus(); - showDropdown = true; - }, - }; - - const methods = { - reset(focus_input = false) { - searchValue = ""; - const copy = options; - for (const entry of copy) { - entry.selected = false; - } - options = methods.get_sorted_array(copy); - if (focus_input) { - searchInputNode?.focus(); - showDropdown = true; - } else { - showDropdown = false; - } - }, - async create_entry(name: string) { - if (!name || !createable || loading) { - console.log("Not sending creation event due to failed preconditions", { name, createable, loading }); - return; - } - try { - await on_create_async({ name }); - searchValue = ""; - loading = false; - } catch (e) { - console.error(e); - } - }, - select_entry(entryId: string) { - if (!entryId || loading) { - console.log("Not selecting entry due to failed preconditions", { - entryId, - loading, - }); - return; - } - - const copy = options; - for (const entry of options) { - if (entry.id === entryId) { - entry.selected = true; - if (multiple) { - searchValue = ""; - } else { - searchValue = entry.name; - } - } else if (!multiple) { - entry.selected = false; - } - } - options = methods.get_sorted_array(copy); - searchInputNode?.focus(); - searchResults = []; - }, - deselect_entry(entryId: string) { - if (!entryId || loading) { - console.log("Not deselecting entry due to failed preconditions", { - entryId, - loading, - }); - return; - } - console.log("Deselecting entry", entryId); - - const copy = options; - - for (const entry of copy) { - if (entry.id === entryId) { - entry.selected = false; - } - } - - options = methods.get_sorted_array(copy); - searchInputNode?.focus(); - }, - get_sorted_array(options: Array<ComboboxOption>): Array<ComboboxOption> { - if (!options) { - return; - } - if (options.length < 1) { - return []; - } - if (searchValue) { - return options; - } - - return options.sort((a, b) => search.normalise_value(a.name).localeCompare(search.normalise_value(b.name))); - }, - }; - - const windowEvents = { - on_mousemove(event: any) { - if (!event.target) return; - mouseIsOverDropdown = event.target?.closest("#" + INTERNAL_ID + " .tongue") != null ?? false; - mouseIsOverComponent = event.target?.closest("#" + INTERNAL_ID) != null ?? false; - }, - on_click() { - if (showDropdown && !mouseIsOverDropdown && !mouseIsOverComponent) { - showDropdown = false; - } - }, - on_keydown(event: any) { - lastKeydownCode = event.code; - const enterPressed = event.code === "Enter"; - const backspacePressed = event.code === "Backspace"; - const arrowUpPressed = event.code === "ArrowUp"; - const spacePressed = event.code === "Space"; - const arrowDownPressed = event.code === "ArrowDown"; - const searchInputHasFocus = element_has_focus(searchInputNode); - const focusedEntry = document.querySelector("#" + INTERNAL_ID + " ul li.focus") as HTMLLIElement; - - if (showDropdown && (enterPressed || arrowDownPressed || arrowUpPressed)) { - event.preventDefault(); - } - - if (searchInputHasFocus && backspacePressed && !searchValue && options.length > 0) { - if (options.filter((c) => c.selected === true).at(-1)?.id ?? false) { - methods.deselect_entry(options.filter((c) => c.selected === true).at(-1)?.id ?? ""); - } - return; - } - - if (searchInputHasFocus && enterPressed && showCreationHint) { - methods.create_entry(searchValue.trim()); - return; - } - - if (searchInputHasFocus && !focusedEntry && arrowDownPressed) { - const firstEntry = document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type"); - if (firstEntry) { - firstEntry.classList.add("focus"); - return; - } - } - - if (focusedEntry && (arrowUpPressed || arrowDownPressed)) { - if (arrowDownPressed) { - if (focusedEntry.nextElementSibling) { - focusedEntry.nextElementSibling.classList.add("focus"); - focusedEntry.nextElementSibling.scrollIntoView(false); - } else { - const firstLIEl = document.querySelector("#" + INTERNAL_ID + " ul li:first-of-type"); - firstLIEl.classList.add("focus"); - firstLIEl.scrollIntoView(false); - } - } else if (arrowUpPressed) { - if (focusedEntry.previousElementSibling) { - focusedEntry.previousElementSibling.classList.add("focus"); - focusedEntry.previousElementSibling.scrollIntoView(false); - } else { - const lastLIEl = document.querySelector("#" + INTERNAL_ID + " ul li:last-of-type"); - lastLIEl.classList.add("focus"); - lastLIEl.scrollIntoView(false); - } - } - focusedEntry.classList.remove("focus"); - return; - } - - if (focusedEntry && (spacePressed || enterPressed)) { - methods.select_entry(focusedEntry.dataset.id); - return; - } - - if (lastKeydownCode === "Tab" && !searchInputHasFocus) { - showDropdown = false; - } - }, - on_touchend(event) { - windowEvents.on_mousemove(event); - }, - }; -</script> - -<svelte:window - on:keydown={windowEvents.on_keydown} - on:mousemove={windowEvents.on_mousemove} - on:touchend={windowEvents.on_touchend} - on:click={windowEvents.on_click} -/> - -<div id={INTERNAL_ID} class:cursor-wait={loading}> - {#if label} - <label for={id} class="block text-sm font-medium text-gray-700"> - {label} - {@html required ? "<span class='text-red-500'>*</span>" : ""} - </label> - {/if} - <div class="relative {label ? 'mt-1' : ''}"> - <div - on:click={search.on_input_wrapper_focus} - on:keypress={search.on_input_wrapper_focus} - class="cursor-text w-full flex rounded-md border bg-white py-2 pl-3 pr-12 sm:text-sm - {inputHasFocus ? `border-${colorName}-500 outline-none ring-1 ring-${colorName}-500` : 'shadow-sm border-gray-300'}" - > - {#if multiple === true && hasSelection} - <div class="flex gap-1 flex-wrap"> - {#each options.filter((c) => c.selected === true) as option} - <Badge - id={option.id} - removable - tabindex="-1" - on:remove={(e) => methods.deselect_entry(e.detail.id)} - text={option.name} - /> - {/each} - </div> - {/if} - <div> - <input - {...attributes} - type="text" - style="all: unset;" - role="combobox" - aria-controls={optionsListId} - aria-expanded={showDropdown} - bind:value={searchValue} - bind:this={searchInputNode} - on:input={() => search.do()} - on:click={search.on_input_click} - on:focus={search.on_input_focus} - on:blur={search.on_input_focusout} - autocomplete="off" - /> - {#if hasSelection} - <button - type="button" - on:click={() => reset()} - title={$LL.reset()} - tabindex="-1" - class="text-gray-400 absolute cursor-pointer inset-y-0 right-0 flex items-center rounded-r-md px-2" - > - <XIcon /> - </button> - {:else} - <span tabindex="-1" class="text-gray-400 absolute inset-y-0 right-0 flex items-center rounded-r-md px-2"> - <ChevronUpDownIcon /> - </span> - {/if} - </div> - </div> - {#if errorText} - <p class="mt-2 text-sm text-red-600" id={ariaErrorDescribedBy}> - {errorText} - </p> - {/if} - <div - class="tongue {showDropdown ? 'absolute' : 'hidden'} - z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white - text-base shadow-lg ring-1 ring-teal ring-opacity-5 focus:outline-none sm:text-sm" - > - <ul id={optionsListId} role="listbox" tabindex="-1"> - {#if searchResults.length > 0} - {#each searchResults.filter((c) => !c.selected) as result} - <li - class="item" - data-id={result.obj.id} - aria-selected={result.obj.selected} - role="option" - on:click={on_select} - on:keypress={on_select} - tabindex="-1" - > - {@html highlight(result, '<span class="font-bold">', "</span>")} - </li> - {/each} - {:else if options.length > 0} - {#each options as option} - <!-- - Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation. - Active: "text-white bg-indigo-600", Not Active: "text-gray-900" - --> - <li - class="item" - aria-selected={option.selected} - role="option" - data-id={option.id} - on:click={on_select} - on:keypress={on_select} - tabindex="-1" - > - <span class="block truncate {option.selected ? 'text-semibold' : ''}">{option.name}</span> - {#if option.selected} - <span class="absolute inset-y-0 right-0 flex items-center pr-4 text-{colorName}-600"> - <CheckCircleIcon /> - </span> - {/if} - </li> - {/each} - {:else} - <slot name="no-records"> - <p class="px-2">{noResultsText}</p> - {#if createable && !searchValue} - <p class="px-2 text-gray-500">{$LL.combobox.createRecordHelpText()}</p> - {/if} - </slot> - {/if} - </ul> - {#if showCreationHint} - <div class="sticky bottom-0 w-full bg-white"> - <Button - text={$LL.combobox.createRecordButtonText(searchValue.trim())} - title={$LL.combobox.createRecordButtonText(searchValue.trim())} - {loading} - kind="reset" - type="button" - on:click={() => methods.create_entry(searchValue.trim())} - /> - </div> - {/if} - </div> - </div> -</div> - -<style lang="postcss"> - .focus { - @apply text-white bg-teal-300; - } - - .item { - @apply relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900; - } - - .item[aria-selected="true"] { - @apply bg-teal-200; - } -</style> diff --git a/code/app/src/lib/components/icons/adjustments.svelte b/code/app/src/lib/components/icons/adjustments.svelte deleted file mode 100644 index 83bda27..0000000 --- a/code/app/src/lib/components/icons/adjustments.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<svg - xmlns="http://www.w3.org/2000/svg" - class="h-6 w-6 {$$restProps.class ?? ''}" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - stroke-width="2" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" - /> -</svg> diff --git a/code/app/src/lib/components/icons/bars-3-center-left.svelte b/code/app/src/lib/components/icons/bars-3-center-left.svelte deleted file mode 100644 index 785ece3..0000000 --- a/code/app/src/lib/components/icons/bars-3-center-left.svelte +++ /dev/null @@ -1,15 +0,0 @@ -<svg - class="h-6 w-6 {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - aria-hidden="true" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M3.75 6.75h16.5M3.75 12H12m-8.25 5.25h16.5" - /> -</svg> diff --git a/code/app/src/lib/components/icons/calendar.svelte b/code/app/src/lib/components/icons/calendar.svelte deleted file mode 100644 index e0053ee..0000000 --- a/code/app/src/lib/components/icons/calendar.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="w-6 h-6 {$$restProps.class ?? ''}" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z" - /> -</svg> diff --git a/code/app/src/lib/components/icons/check-circle.svelte b/code/app/src/lib/components/icons/check-circle.svelte deleted file mode 100644 index e30778e..0000000 --- a/code/app/src/lib/components/icons/check-circle.svelte +++ /dev/null @@ -1,13 +0,0 @@ -<svg - class="h-5 w-5 {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" -> - <path - fill-rule="evenodd" - d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/chevron-down.svelte b/code/app/src/lib/components/icons/chevron-down.svelte deleted file mode 100644 index 5b29ece..0000000 --- a/code/app/src/lib/components/icons/chevron-down.svelte +++ /dev/null @@ -1,7 +0,0 @@ -<svg class="h-5 w-5 {$$restProps.class ?? ''}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> - <path - fill-rule="evenodd" - d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/chevron-up-down.svelte b/code/app/src/lib/components/icons/chevron-up-down.svelte deleted file mode 100644 index c07aed5..0000000 --- a/code/app/src/lib/components/icons/chevron-up-down.svelte +++ /dev/null @@ -1,13 +0,0 @@ -<svg - class="h-5 w-5 {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" -> - <path - fill-rule="evenodd" - d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/chevron-up.svelte b/code/app/src/lib/components/icons/chevron-up.svelte deleted file mode 100644 index 289e71d..0000000 --- a/code/app/src/lib/components/icons/chevron-up.svelte +++ /dev/null @@ -1,7 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> - <path - fill-rule="evenodd" - d="M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/database.svelte b/code/app/src/lib/components/icons/database.svelte deleted file mode 100644 index 6ffdadb..0000000 --- a/code/app/src/lib/components/icons/database.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<svg - xmlns="http://www.w3.org/2000/svg" - class="h-6 w-6 {$$restProps.class ?? ''}" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - stroke-width="2" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" - /> -</svg> diff --git a/code/app/src/lib/components/icons/exclamation-circle.svelte b/code/app/src/lib/components/icons/exclamation-circle.svelte deleted file mode 100644 index 2ce79b1..0000000 --- a/code/app/src/lib/components/icons/exclamation-circle.svelte +++ /dev/null @@ -1,13 +0,0 @@ -<svg - class="h-5 w-5 {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" -> - <path - fill-rule="evenodd" - d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/exclamation-triangle.svelte b/code/app/src/lib/components/icons/exclamation-triangle.svelte deleted file mode 100644 index 8d807db..0000000 --- a/code/app/src/lib/components/icons/exclamation-triangle.svelte +++ /dev/null @@ -1,13 +0,0 @@ -<svg - class="h-5 w-5 {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" -> - <path - fill-rule="evenodd" - d="M8.485 3.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 3.495zM10 6a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 6zm0 9a1 1 0 100-2 1 1 0 000 2z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/folder-open.svelte b/code/app/src/lib/components/icons/folder-open.svelte deleted file mode 100644 index 409c8e2..0000000 --- a/code/app/src/lib/components/icons/folder-open.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="w-6 h-6 {$$restProps.class ?? ''}" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776" - /> -</svg> diff --git a/code/app/src/lib/components/icons/funnel.svelte b/code/app/src/lib/components/icons/funnel.svelte deleted file mode 100644 index 7e9daeb..0000000 --- a/code/app/src/lib/components/icons/funnel.svelte +++ /dev/null @@ -1,7 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> - <path - fill-rule="evenodd" - d="M2.628 1.601C5.028 1.206 7.49 1 10 1s4.973.206 7.372.601a.75.75 0 01.628.74v2.288a2.25 2.25 0 01-.659 1.59l-4.682 4.683a2.25 2.25 0 00-.659 1.59v3.037c0 .684-.31 1.33-.844 1.757l-1.937 1.55A.75.75 0 018 18.25v-5.757a2.25 2.25 0 00-.659-1.591L2.659 6.22A2.25 2.25 0 012 4.629V2.34a.75.75 0 01.628-.74z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/home.svelte b/code/app/src/lib/components/icons/home.svelte deleted file mode 100644 index ee8305d..0000000 --- a/code/app/src/lib/components/icons/home.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<svg - xmlns="http://www.w3.org/2000/svg" - class="h-6 w-6 {$$restProps.class ?? ''}" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - stroke-width="2" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" - /> -</svg> diff --git a/code/app/src/lib/components/icons/index.ts b/code/app/src/lib/components/icons/index.ts deleted file mode 100644 index eb5b439..0000000 --- a/code/app/src/lib/components/icons/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import XIcon from "./x.svelte"; -import MenuIcon from "./menu.svelte"; -import AdjustmentsIcon from "./adjustments.svelte"; -import DatabaseIcon from "./database.svelte"; -import HomeIcon from "./home.svelte"; -import InformationCircleIcon from "./information-circle.svelte"; -import ExclamationTriangleIcon from "./exclamation-triangle.svelte"; -import XCircleIcon from "./x-circle.svelte"; -import CheckCircleIcon from "./check-circle.svelte"; -import XMarkIcon from "./x-mark.svelte"; -import SpinnerIcon from "./spinner.svelte"; -import ExclamationCircleIcon from "./exclamation-circle.svelte"; -import ChevronUpDownIcon from "./chevron-up-down.svelte"; -import MagnifyingGlassIcon from "./magnifying-glass.svelte"; -import Bars3CenterLeftIcon from "./bars-3-center-left.svelte"; -import CalendarIcon from "./calendar.svelte"; -import FolderOpenIcon from "./folder-open.svelte"; -import MegaphoneIcon from "./megaphone.svelte"; -import QueueListIcon from "./queue-list.svelte"; -import ChevronDownIcon from "./chevron-down.svelte"; -import ChevronUpIcon from "./chevron-up.svelte"; -import FunnelIcon from "./funnel.svelte"; - -export { - FunnelIcon, - ChevronDownIcon, - ChevronUpIcon, - QueueListIcon, - FolderOpenIcon, - MegaphoneIcon, - CalendarIcon, - Bars3CenterLeftIcon, - MagnifyingGlassIcon, - ChevronUpDownIcon, - XIcon, - MenuIcon, - HomeIcon, - DatabaseIcon, - AdjustmentsIcon, - InformationCircleIcon, - ExclamationTriangleIcon, - ExclamationCircleIcon, - XCircleIcon, - CheckCircleIcon, - XMarkIcon, - SpinnerIcon -}
\ No newline at end of file diff --git a/code/app/src/lib/components/icons/information-circle.svelte b/code/app/src/lib/components/icons/information-circle.svelte deleted file mode 100644 index 68dbc60..0000000 --- a/code/app/src/lib/components/icons/information-circle.svelte +++ /dev/null @@ -1,13 +0,0 @@ -<svg - class="h-5 w-5 {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" -> - <path - fill-rule="evenodd" - d="M19 10.5a8.5 8.5 0 11-17 0 8.5 8.5 0 0117 0zM8.25 9.75A.75.75 0 019 9h.253a1.75 1.75 0 011.709 2.13l-.46 2.066a.25.25 0 00.245.304H11a.75.75 0 010 1.5h-.253a1.75 1.75 0 01-1.709-2.13l.46-2.066a.25.25 0 00-.245-.304H9a.75.75 0 01-.75-.75zM10 7a1 1 0 100-2 1 1 0 000 2z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/magnifying-glass.svelte b/code/app/src/lib/components/icons/magnifying-glass.svelte deleted file mode 100644 index f8fdb6e..0000000 --- a/code/app/src/lib/components/icons/magnifying-glass.svelte +++ /dev/null @@ -1,13 +0,0 @@ -<svg - class="h-5 w-5 {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" -> - <path - fill-rule="evenodd" - d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/megaphone.svelte b/code/app/src/lib/components/icons/megaphone.svelte deleted file mode 100644 index 7ada5f3..0000000 --- a/code/app/src/lib/components/icons/megaphone.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="w-6 h-6 {$$restProps.class ?? ''}" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46" - /> -</svg> diff --git a/code/app/src/lib/components/icons/menu.svelte b/code/app/src/lib/components/icons/menu.svelte deleted file mode 100644 index 471d85f..0000000 --- a/code/app/src/lib/components/icons/menu.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<svg - xmlns="http://www.w3.org/2000/svg" - class="h-6 w-6 {$$restProps.class ?? ''}" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - stroke-width="2" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M4 6h16M4 12h16M4 18h16" - /> -</svg> diff --git a/code/app/src/lib/components/icons/queue-list.svelte b/code/app/src/lib/components/icons/queue-list.svelte deleted file mode 100644 index 6148394..0000000 --- a/code/app/src/lib/components/icons/queue-list.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="w-6 h-6 {$$restProps.class ?? ''}" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z" - /> -</svg> diff --git a/code/app/src/lib/components/icons/spinner.svelte b/code/app/src/lib/components/icons/spinner.svelte deleted file mode 100644 index 80cc57c..0000000 --- a/code/app/src/lib/components/icons/spinner.svelte +++ /dev/null @@ -1,20 +0,0 @@ -<svg - class="-ml-1 mr-3 h-5 w-5 animate-spin {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" -> - <circle - class="opacity-25" - cx="12" - cy="12" - r="10" - stroke="currentColor" - stroke-width="4" - /> - <path - class="opacity-75" - fill="currentColor" - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" - /> -</svg> diff --git a/code/app/src/lib/components/icons/x-circle.svelte b/code/app/src/lib/components/icons/x-circle.svelte deleted file mode 100644 index 3793b5a..0000000 --- a/code/app/src/lib/components/icons/x-circle.svelte +++ /dev/null @@ -1,13 +0,0 @@ -<svg - class="h-5 w-5 {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" -> - <path - fill-rule="evenodd" - d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" - clip-rule="evenodd" - /> -</svg> diff --git a/code/app/src/lib/components/icons/x-mark.svelte b/code/app/src/lib/components/icons/x-mark.svelte deleted file mode 100644 index fd1c6a1..0000000 --- a/code/app/src/lib/components/icons/x-mark.svelte +++ /dev/null @@ -1,11 +0,0 @@ -<svg - class="h-5 w-5 {$$restProps.class ?? ''}" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" -> - <path - d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" - /> -</svg> diff --git a/code/app/src/lib/components/icons/x.svelte b/code/app/src/lib/components/icons/x.svelte deleted file mode 100644 index 6125ab8..0000000 --- a/code/app/src/lib/components/icons/x.svelte +++ /dev/null @@ -1,14 +0,0 @@ -<svg - xmlns="http://www.w3.org/2000/svg" - class="h-6 w-6 {$$restProps.class ?? ''}" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - stroke-width="2" -> - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M6 18L18 6M6 6l12 12" - /> -</svg> diff --git a/code/app/src/lib/components/index.ts b/code/app/src/lib/components/index.ts deleted file mode 100644 index d6abd4c..0000000 --- a/code/app/src/lib/components/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Alert from "./alert.svelte"; -import Button from "./button.svelte"; -import Checkbox from "./checkbox.svelte"; -import Input from "./input.svelte"; -import LocaleSwitcher from "./locale-switcher.svelte"; -import Switch from "./switch.svelte"; -import Badge from "./badge.svelte"; -import ProjectStatusBadge from "./project-status-badge.svelte"; -import TextArea from "./textarea.svelte"; -import Combobox from "./combobox.svelte"; - -export { - Badge, - Combobox, - TextArea, - ProjectStatusBadge, - Alert, - Button, - Checkbox, - Input, - LocaleSwitcher, - Switch -}
\ No newline at end of file diff --git a/code/app/src/lib/components/input.svelte b/code/app/src/lib/components/input.svelte deleted file mode 100644 index 80b1543..0000000 --- a/code/app/src/lib/components/input.svelte +++ /dev/null @@ -1,113 +0,0 @@ -<script lang="ts"> - import pwKey from "$actions/pwKey"; - import { random_string } from "$lib/helpers"; - import { error } from "@sveltejs/kit"; - import { ExclamationCircleIcon } from "./icons"; - - export let label: string | undefined = undefined; - export let type: string = "text"; - export let autocomplete: string | undefined = undefined; - export let required: boolean | undefined = undefined; - export let id: string | undefined = "input__" + random_string(4); - export let name: string | undefined = undefined; - export let placeholder: string | undefined = undefined; - export let helpText: string | undefined = undefined; - export let errorText: string | undefined = undefined; - export let errors: Array<string> | undefined = undefined; - export let disabled = false; - export let hideLabel = false; - export let cornerHint: string | undefined = undefined; - export let icon: any = undefined; - export let addon: string | undefined = undefined; - export let value: string | undefined; - export let wrapperClass: string | undefined = undefined; - export let _pwKey: string | undefined = undefined; - - $: ariaErrorDescribedBy = id + "__" + "error"; - $: attributes = { - "aria-describedby": errorText || errors?.length ? ariaErrorDescribedBy : null, - "aria-invalid": errorText || errors?.length ? "true" : null, - disabled: disabled || null, - autocomplete: autocomplete || null, - required: required || null, - } as any; - $: hasBling = icon || addon || errorText; - const defaultColorClass = "border-gray-300 focus:border-teal-500 focus:ring-teal-500"; - let colorClass = defaultColorClass; - $: if (errorText) { - colorClass = "placeholder-red-300 focus:border-red-500 focus:outline-none focus:ring-red-500 text-red-900 pr-10 border-red-300"; - } else { - colorClass = defaultColorClass; - } - - function typeAction(node: HTMLInputElement) { - node.type = type; - } -</script> - -<div class={wrapperClass}> - {#if label && !cornerHint && !hideLabel} - <label for={id} class={hideLabel ? "sr-only" : "block text-sm font-medium text-gray-700"}> - {label} - {@html required ? "<span class='text-red-500'>*</span>" : ""} - </label> - {:else if cornerHint && !hideLabel} - <div class="flex justify-between"> - {#if label} - <label for={id} class={hideLabel ? "sr-only" : "block text-sm font-medium text-gray-700"}> - {label} - {@html required ? "<span class='text-red-500'>*</span>" : ""} - </label> - {/if} - <span class="text-sm text-gray-500"> - {cornerHint} - </span> - </div> - {/if} - <div class="{label ? 'mt-1' : ''} {hasBling ? 'relative rounded-md' : ''} {addon ? 'flex' : ''}"> - {#if icon} - <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> - <svelte:component this={icon} class={errorText ? "text-red-500" : "text-gray-400"} /> - </div> - {:else if addon} - <div class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-gray-500 sm:text-sm"> - <span class="text-gray-500 sm:text-sm">{addon}</span> - </div> - {/if} - <input - use:typeAction - use:pwKey={_pwKey} - {name} - {id} - {...attributes} - bind:value - class="block w-full rounded-md shadow-sm sm:text-sm - {colorClass} - {disabled ? 'disabled:cursor-not-allowed disabled:border-gray-200 disabled:bg-gray-50 disabled:text-gray-500' : ''} - {addon ? 'min-w-0 flex-1 rounded-none rounded-r-md' : ''} - {icon ? 'pl-10' : ''}" - {placeholder} - /> - {#if errorText} - <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"> - <ExclamationCircleIcon class="text-red-500" /> - </div> - {/if} - </div> - {#if helpText && !errorText} - <p class="mt-2 text-sm text-gray-500"> - {helpText} - </p> - {/if} - {#if errorText || errors?.length === 1} - <p class="mt-2 text-sm text-red-600" id={ariaErrorDescribedBy}> - {errorText ?? errors[0]} - </p> - {:else if errors && errors.length} - <ul class="mt-2 list-disc" id={ariaErrorDescribedBy}> - {#each errors as error} - <li class="text-sm text-red-600">{error}</li> - {/each} - </ul> - {/if} -</div> diff --git a/code/app/src/lib/components/locale-switcher.svelte b/code/app/src/lib/components/locale-switcher.svelte deleted file mode 100644 index 3681bf5..0000000 --- a/code/app/src/lib/components/locale-switcher.svelte +++ /dev/null @@ -1,56 +0,0 @@ -<script lang="ts"> - import pwKey from "$actions/pwKey"; - import { browser } from "$app/environment"; - import { page } from "$app/stores"; - import { CookieNames } from "$lib/configuration"; - import { setLocale, locale } from "$lib/i18n/i18n-svelte"; - import type { Locales } from "$lib/i18n/i18n-types"; - import { locales } from "$lib/i18n/i18n-util"; - import { loadLocaleAsync } from "$lib/i18n/i18n-util.async"; - import Cookies from "js-cookie"; - - export let _pwKey: string | undefined = undefined; - export let tabindex: number | undefined = undefined; - let currentLocale = Cookies.get(CookieNames.locale); - - async function switch_locale(newLocale: Locales) { - if (!newLocale || $locale === newLocale) return; - await loadLocaleAsync(newLocale); - setLocale(newLocale); - document.querySelector("html")?.setAttribute("lang", newLocale); - Cookies.set(CookieNames.locale, newLocale); - currentLocale = newLocale; - console.log("Switched to: " + newLocale); - } - - function on_change(event: Event) { - const target = event.target as HTMLSelectElement; - switch_locale(target.options[target.selectedIndex].value as Locales); - } - - $: if (browser) { - switch_locale($page.params.lang as Locales); - } - - function get_locale_name(iso: string) { - switch (iso) { - case "nb": { - return "Norsk Bokmål"; - } - case "en": { - return "English"; - } - } - } -</script> - -<select - {tabindex} - use:pwKey={_pwKey} - on:change={on_change} - class="mt-1 mr-1 block border-none py-2 pl-3 pr-10 text-base rounded-md right-0 absolute focus:outline-none focus:ring-teal-500 sm:text-sm" -> - {#each locales as aLocale} - <option value={aLocale} selected={aLocale === currentLocale}>{get_locale_name(aLocale)}</option> - {/each} -</select> diff --git a/code/app/src/lib/components/project-status-badge.svelte b/code/app/src/lib/components/project-status-badge.svelte deleted file mode 100644 index 5390344..0000000 --- a/code/app/src/lib/components/project-status-badge.svelte +++ /dev/null @@ -1,24 +0,0 @@ -<script lang="ts"> - import type { ProjectStatus } from "$lib/models/projects/ProjectStatus"; - import Badge from "./badge.svelte"; - export let status: string | ProjectStatus; - - let text = ""; - let type = "default" as any; - $: switch (status) { - case "idl": - type = "tame"; - text = "IDLE"; - break; - case "exp": - type = "yellow"; - text = "EXPIRED"; - break; - case "act": - type = "green"; - text = "ACTIVE"; - break; - } -</script> - -<Badge {text} {type} uppercase /> diff --git a/code/app/src/lib/components/switch.svelte b/code/app/src/lib/components/switch.svelte deleted file mode 100644 index 79f2d67..0000000 --- a/code/app/src/lib/components/switch.svelte +++ /dev/null @@ -1,125 +0,0 @@ -<script context="module" lang="ts"> - export type SwitchType = "short" | "icon" | "default"; -</script> - -<script lang="ts"> - import pwKey from "$actions/pwKey"; - - export let enabled = false; - export let type: SwitchType = "default"; - export let srText = "Use setting"; - export let label: string | undefined = undefined; - export let description: string | undefined = undefined; - export let rightAlignedLabelDescription = false; - export let _pwKey: string | undefined = undefined; - - $: colorClass = enabled ? "bg-teal-600 focus:ring-teal-500" : "bg-gray-200 focus:ring-teal-500"; - $: translateClass = enabled ? "translate-x-5" : "translate-x-0"; - $: hasLabelOrDescription = label || description; - - function toggle() { - enabled = !enabled; - } -</script> - -<div class="{hasLabelOrDescription ? 'flex items-center' : ''} {rightAlignedLabelDescription ? '' : 'justify-between'}"> - {#if hasLabelOrDescription && !rightAlignedLabelDescription} - <span class="flex flex-grow flex-col"> - {#if label} - <span class="text-sm font-medium text-gray-900">{label}</span> - {/if} - {#if description} - <span class="text-sm text-gray-500">{description}</span> - {/if} - </span> - {/if} - {#if type === "short"} - <button - type="button" - class="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2" - role="switch" - aria-checked={enabled} - use:pwKey={_pwKey} - on:click={toggle} - > - <span class="sr-only">{srText}</span> - <span aria-hidden="true" class="pointer-events-none absolute h-full w-full rounded-md" /> - <span - aria-hidden="true" - class="{colorClass} pointer-events-none absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out" - /> - <span - aria-hidden="true" - class="{translateClass} pointer-events-none absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow ring-0 transition-transform duration-200 ease-in-out" - /> - </button> - {:else if type === "icon"} - <button - type="button" - class="{colorClass} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2" - role="switch" - aria-checked={enabled} - use:pwKey={_pwKey} - on:click={toggle} - > - <span class="sr-only">{srText}</span> - <span - class="{translateClass} pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" - > - <span - class="{enabled - ? 'opacity-0 ease-out duration-100' - : 'opacity-100 ease-in duration-200'} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" - aria-hidden="true" - > - <svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12"> - <path - d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" - stroke="currentColor" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" - /> - </svg> - </span> - <span - class="{enabled - ? 'opacity-100 ease-in duration-200' - : 'opacity-0 ease-out duration-100'} absolute inset-0 flex h-full w-full items-center justify-center transition-opacity" - aria-hidden="true" - > - <svg class="h-3 w-3 text-indigo-600" fill="currentColor" viewBox="0 0 12 12"> - <path - d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" - /> - </svg> - </span> - </span> - </button> - {:else if type === "default"} - <button - type="button" - class="{colorClass} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2" - role="switch" - aria-checked={enabled} - use:pwKey={_pwKey} - on:click={toggle} - > - <span class="sr-only">{srText}</span> - <span - aria-hidden="true" - class="{translateClass} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" - /> - </button> - {/if} - {#if hasLabelOrDescription && rightAlignedLabelDescription} - <span class="ml-3"> - {#if label} - <span class="text-sm font-medium text-gray-900">{label}</span> - {/if} - {#if description} - <span class="text-sm text-gray-500">{description}</span> - {/if} - </span> - {/if} -</div> diff --git a/code/app/src/lib/components/textarea.svelte b/code/app/src/lib/components/textarea.svelte deleted file mode 100644 index a3dd06a..0000000 --- a/code/app/src/lib/components/textarea.svelte +++ /dev/null @@ -1,81 +0,0 @@ -<script lang="ts"> - import { random_string } from "$lib/helpers"; - - export let id = "textarea-" + random_string(4); - export let disabled = false; - export let rows = 2; - export let cols = 0; - export let name = ""; - export let placeholder = ""; - export let value; - export let label = ""; - export let required = false; - export let errorText = ""; - export let errors: Array<string> | undefined = undefined; - - $: ariaErrorDescribedBy = id + "__" + "error"; - $: attributes = { - "aria-describedby": errorText || errors?.length ? ariaErrorDescribedBy : null, - "aria-invalid": errorText || errors?.length ? "true" : null, - rows: rows || null, - cols: cols || null, - name: name || null, - id: id || null, - disabled: disabled || null, - required: required || null, - } as any; - - let textareaElement; - let scrollHeight = 0; - const defaultColorClass = "border-gray-300 focus:border-teal-500 focus:ring-teal-500"; - let colorClass = defaultColorClass; - - $: if (errorText) { - colorClass = "placeholder-red-300 focus:border-red-500 focus:outline-none focus:ring-red-500 text-red-900 pr-10 border-red-300"; - } else { - colorClass = defaultColorClass; - } - - $: if (textareaElement) { - scrollHeight = textareaElement.scrollHeight; - } - - function on_input(event) { - event.target.style.height = "auto"; - event.target.style.height = this.scrollHeight + "px"; - } -</script> - -<div> - {#if label} - <label for={id} class="block text-sm font-medium text-gray-700"> - {label} - {@html required ? "<span class='text-red-500'>*</span>" : ""} - </label> - {/if} - <div class="mt-1"> - <textarea - {rows} - {name} - {id} - {...attributes} - style="overflow-y:hidden;min-height:calc(1.5em + .75rem + 2px);{scrollHeight ? 'height:{scrollHeight}px' : ''};" - bind:value - bind:this={textareaElement} - on:input={on_input} - {placeholder} - class="block w-full rounded-md {colorClass} shadow-sm sm:text-sm" - /> - {#if errorText || errors?.length === 1} - <p class="mt-2 text-sm text-red-600" id={ariaErrorDescribedBy}> - {errorText ?? errors[0]} - </p> - {:else if errors && errors.length} - <ul class="mt-2 list-disc" id={ariaErrorDescribedBy}> - {#each errors as error} - <li class="text-sm text-red-600">{error}</li> - {/each} - </ul> - {/if} - </div> -</div> diff --git a/code/app/src/lib/configuration.ts b/code/app/src/lib/configuration.ts deleted file mode 100644 index 8debef6..0000000 --- a/code/app/src/lib/configuration.ts +++ /dev/null @@ -1,60 +0,0 @@ -export const BASE_DOMAIN = "dev.greatoffice.life"; -export const DEV_BASE_DOMAIN = "http://localhost"; -export const API_ADDRESS = "https://api." + BASE_DOMAIN; -export const DEV_API_ADDRESS = "http://localhost:5000"; -export const SECONDS_BETWEEN_SESSION_CHECK = 600; - -export function api_base(path: string = "", useDefaultVersion = false): string { - return (is_development() ? DEV_API_ADDRESS : API_ADDRESS) + (path !== "" ? "/" + path : ""); -} - -export function is_development(): boolean { - return import.meta.env.DEV; -} - -export function is_testing(): boolean { - return import.meta.env.VITE_TESTING; -} - -export function is_debug(): boolean { - return localStorage.getItem(StorageKeys.debug) !== "true"; -} - -export const CookieNames = { - theme: "go_theme", - locale: "go_locale", - session: "go_session" -}; - -export function get_test_context(): TestContext { - return { - user: { - username: import.meta.env.VITE_TEST_USERNAME, - password: import.meta.env.VITE_TEST_PASSWORD - } - } -} - -export interface TestContext { - user: { - username: string, - password: string - } -} - -export const QueryKeys = { - labels: "labels", - categories: "categories", - entries: "entries", -}; - -export const StorageKeys = { - session: "sessionData", - theme: "theme", - debug: "debug", - categories: "categories", - labels: "labels", - entries: "entries", - stopwatch: "stopwatchState", - logLevel: "logLevel" -};
\ No newline at end of file diff --git a/code/app/src/lib/helpers.ts b/code/app/src/lib/helpers.ts deleted file mode 100644 index 4584de7..0000000 --- a/code/app/src/lib/helpers.ts +++ /dev/null @@ -1,464 +0,0 @@ -import { browser } from "$app/environment"; -import type { WorkEntry } from "$lib/models/work/WorkEntry"; -import { log_info } from "$lib/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/lib/i18n/en/app/index.ts b/code/app/src/lib/i18n/en/app/index.ts deleted file mode 100644 index 7ccfc97..0000000 --- a/code/app/src/lib/i18n/en/app/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { BaseTranslation } from '../../i18n-types' - -const en_app: BaseTranslation = { - members: "Members", -} - -export default en_app
\ No newline at end of file diff --git a/code/app/src/lib/i18n/en/index.ts b/code/app/src/lib/i18n/en/index.ts deleted file mode 100644 index fbf5423..0000000 --- a/code/app/src/lib/i18n/en/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { BaseTranslation } from "../i18n-types"; - -const en: BaseTranslation = { - or: "Or", - name: "Name", - emailAddress: "Email address", - password: "Password", - pageNotFound: "Page not found", - noInternet: "It seems like your device does not have a internet connection, please check your connection.", - reset: "Reset", - of: "{0} of {1}", - isRequired: "{0} is required", - submit: "Submit", - success: "Success", - tryAgainSoon: "Try again soon", - createANewAccount: "Create a new account", - unexpectedError: "An unexpected error occured", - notFound: "Not found", - documentation: "Documentation", - tos: "Terms of service", - privacyPolicy: "Privacy policy", - signIntoYourAccount: "Sign into your account", - combobox: { - search: "Search", - noRecordsFound: "No records found", - createRecordHelpText: "Create a record by typing the name in the search bar and pressing enter", - createRecordButtonText: "Press enter or click here to create {0}" - }, - signInPage: { - notMyComputer: "This is not my computer", - resetPassword: "Reset password", - yourPasswordIsUpdated: "Your password is updated", - signIn: "Sign In", - yourNewPasswordIsApplied: "Your new password is applied", - signInBelow: "Sign in below", - yourAccountIsDisabled: "Your account is disabled", - contactYourAdminIfDisabled: "Contact your administrator if this feels wrong", - youHaveReachedInactivityLimit: "You've reached the hidden inactivity limit", - feelFreeToSignInAgain: "Feel free to sign in again" - }, - signUpPage: { - createYourNewAccount: "Create your new account", - }, - resetPasswordPage: { - setANewPassword: "Set a new password", - expired: "Expired", - requestHasExpired: "Your request has expired", - requestANewReset: "Request a new reset", - invalidRequestTitle: "Your request is invalid", - invalidRequestMessage: "This could be due to it being expired, nonexsistent or something else", - newPassword: "New password", - requestSentMessage: "If we find your email address in our systems, you will receive an email with instructions on how to set a new password for your account.", - requestAPasswordReset: "Request a password reset", - requestNotFound: "Your request was not found", - submitANewRequestBelow: "Submit a new reset request below" - } -}; - -export default en; diff --git a/code/app/src/lib/i18n/formatters.ts b/code/app/src/lib/i18n/formatters.ts deleted file mode 100644 index 5232b7d..0000000 --- a/code/app/src/lib/i18n/formatters.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { capitalise } from '$lib/helpers' -import type { FormattersInitializer } from 'typesafe-i18n' -import type { Locales, Formatters } from './i18n-types' - -export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => { - - const formatters: Formatters = { - // add your formatter functions here - capitalise: (value: string) => capitalise(value) - } - - return formatters -} diff --git a/code/app/src/lib/i18n/i18n-svelte.ts b/code/app/src/lib/i18n/i18n-svelte.ts deleted file mode 100644 index 6cdffb3..0000000 --- a/code/app/src/lib/i18n/i18n-svelte.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. -/* eslint-disable */ - -import { initI18nSvelte } from 'typesafe-i18n/svelte' -import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types' -import { loadedFormatters, loadedLocales } from './i18n-util' - -const { locale, LL, setLocale } = initI18nSvelte<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters) - -export { locale, LL, setLocale } - -export default LL diff --git a/code/app/src/lib/i18n/i18n-types.ts b/code/app/src/lib/i18n/i18n-types.ts deleted file mode 100644 index cf968d7..0000000 --- a/code/app/src/lib/i18n/i18n-types.ts +++ /dev/null @@ -1,429 +0,0 @@ -// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. -/* eslint-disable */ -import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n' - -export type BaseTranslation = BaseTranslationType & DisallowNamespaces -export type BaseLocale = 'en' - -export type Locales = - | 'en' - | 'nb' - -export type Translation = RootTranslation & DisallowNamespaces - -export type Translations = RootTranslation & -{ - app: NamespaceAppTranslation -} - -type RootTranslation = { - /** - * Or - */ - or: string - /** - * Name - */ - name: string - /** - * Email address - */ - emailAddress: string - /** - * Password - */ - password: string - /** - * Page not found - */ - pageNotFound: string - /** - * It seems like your device does not have a internet connection, please check your connection. - */ - noInternet: string - /** - * Reset - */ - reset: string - /** - * {0} of {1} - * @param {unknown} 0 - * @param {unknown} 1 - */ - of: RequiredParams<'0' | '1'> - /** - * {0} is required - * @param {unknown} 0 - */ - isRequired: RequiredParams<'0'> - /** - * Submit - */ - submit: string - /** - * Success - */ - success: string - /** - * Try again soon - */ - tryAgainSoon: string - /** - * Create a new account - */ - createANewAccount: string - /** - * An unexpected error occured - */ - unexpectedError: string - /** - * Not found - */ - notFound: string - /** - * Documentation - */ - documentation: string - /** - * Terms of service - */ - tos: string - /** - * Privacy policy - */ - privacyPolicy: string - /** - * Sign into your account - */ - signIntoYourAccount: string - combobox: { - /** - * Search - */ - search: string - /** - * No records found - */ - noRecordsFound: string - /** - * Create a record by typing the name in the search bar and pressing enter - */ - createRecordHelpText: string - /** - * Press enter or click here to create {0} - * @param {unknown} 0 - */ - createRecordButtonText: RequiredParams<'0'> - } - signInPage: { - /** - * This is not my computer - */ - notMyComputer: string - /** - * Reset password - */ - resetPassword: string - /** - * Your password is updated - */ - yourPasswordIsUpdated: string - /** - * Sign In - */ - signIn: string - /** - * Your new password is applied - */ - yourNewPasswordIsApplied: string - /** - * Sign in below - */ - signInBelow: string - /** - * Your account is disabled - */ - yourAccountIsDisabled: string - /** - * Contact your administrator if this feels wrong - */ - contactYourAdminIfDisabled: string - /** - * You've reached the hidden inactivity limit - */ - youHaveReachedInactivityLimit: string - /** - * Feel free to sign in again - */ - feelFreeToSignInAgain: string - } - signUpPage: { - /** - * Create your new account - */ - createYourNewAccount: string - } - resetPasswordPage: { - /** - * Set a new password - */ - setANewPassword: string - /** - * Expired - */ - expired: string - /** - * Your request has expired - */ - requestHasExpired: string - /** - * Request a new reset - */ - requestANewReset: string - /** - * Your request is invalid - */ - invalidRequestTitle: string - /** - * This could be due to it being expired, nonexsistent or something else - */ - invalidRequestMessage: string - /** - * New password - */ - newPassword: string - /** - * If we find your email address in our systems, you will receive an email with instructions on how to set a new password for your account. - */ - requestSentMessage: string - /** - * Request a password reset - */ - requestAPasswordReset: string - /** - * Your request was not found - */ - requestNotFound: string - /** - * Submit a new reset request below - */ - submitANewRequestBelow: string - } -} - -export type NamespaceAppTranslation = { - /** - * Members - */ - members: string -} - -export type Namespaces = - | 'app' - -type DisallowNamespaces = { - /** - * reserved for 'app'-namespace\ - * you need to use the `./app/index.ts` file instead - */ - app?: "[typesafe-i18n] reserved for 'app'-namespace. You need to use the `./app/index.ts` file instead." -} - -export type TranslationFunctions = { - /** - * Or - */ - or: () => LocalizedString - /** - * Name - */ - name: () => LocalizedString - /** - * Email address - */ - emailAddress: () => LocalizedString - /** - * Password - */ - password: () => LocalizedString - /** - * Page not found - */ - pageNotFound: () => LocalizedString - /** - * It seems like your device does not have a internet connection, please check your connection. - */ - noInternet: () => LocalizedString - /** - * Reset - */ - reset: () => LocalizedString - /** - * {0} of {1} - */ - of: (arg0: unknown, arg1: unknown) => LocalizedString - /** - * {0} is required - */ - isRequired: (arg0: unknown) => LocalizedString - /** - * Submit - */ - submit: () => LocalizedString - /** - * Success - */ - success: () => LocalizedString - /** - * Try again soon - */ - tryAgainSoon: () => LocalizedString - /** - * Create a new account - */ - createANewAccount: () => LocalizedString - /** - * An unexpected error occured - */ - unexpectedError: () => LocalizedString - /** - * Not found - */ - notFound: () => LocalizedString - /** - * Documentation - */ - documentation: () => LocalizedString - /** - * Terms of service - */ - tos: () => LocalizedString - /** - * Privacy policy - */ - privacyPolicy: () => LocalizedString - /** - * Sign into your account - */ - signIntoYourAccount: () => LocalizedString - combobox: { - /** - * Search - */ - search: () => LocalizedString - /** - * No records found - */ - noRecordsFound: () => LocalizedString - /** - * Create a record by typing the name in the search bar and pressing enter - */ - createRecordHelpText: () => LocalizedString - /** - * Press enter or click here to create {0} - */ - createRecordButtonText: (arg0: unknown) => LocalizedString - } - signInPage: { - /** - * This is not my computer - */ - notMyComputer: () => LocalizedString - /** - * Reset password - */ - resetPassword: () => LocalizedString - /** - * Your password is updated - */ - yourPasswordIsUpdated: () => LocalizedString - /** - * Sign In - */ - signIn: () => LocalizedString - /** - * Your new password is applied - */ - yourNewPasswordIsApplied: () => LocalizedString - /** - * Sign in below - */ - signInBelow: () => LocalizedString - /** - * Your account is disabled - */ - yourAccountIsDisabled: () => LocalizedString - /** - * Contact your administrator if this feels wrong - */ - contactYourAdminIfDisabled: () => LocalizedString - /** - * You've reached the hidden inactivity limit - */ - youHaveReachedInactivityLimit: () => LocalizedString - /** - * Feel free to sign in again - */ - feelFreeToSignInAgain: () => LocalizedString - } - signUpPage: { - /** - * Create your new account - */ - createYourNewAccount: () => LocalizedString - } - resetPasswordPage: { - /** - * Set a new password - */ - setANewPassword: () => LocalizedString - /** - * Expired - */ - expired: () => LocalizedString - /** - * Your request has expired - */ - requestHasExpired: () => LocalizedString - /** - * Request a new reset - */ - requestANewReset: () => LocalizedString - /** - * Your request is invalid - */ - invalidRequestTitle: () => LocalizedString - /** - * This could be due to it being expired, nonexsistent or something else - */ - invalidRequestMessage: () => LocalizedString - /** - * New password - */ - newPassword: () => LocalizedString - /** - * If we find your email address in our systems, you will receive an email with instructions on how to set a new password for your account. - */ - requestSentMessage: () => LocalizedString - /** - * Request a password reset - */ - requestAPasswordReset: () => LocalizedString - /** - * Your request was not found - */ - requestNotFound: () => LocalizedString - /** - * Submit a new reset request below - */ - submitANewRequestBelow: () => LocalizedString - } - app: { - /** - * Members - */ - members: () => LocalizedString - } -} - -export type Formatters = {} diff --git a/code/app/src/lib/i18n/i18n-util.async.ts b/code/app/src/lib/i18n/i18n-util.async.ts deleted file mode 100644 index 2e6717e..0000000 --- a/code/app/src/lib/i18n/i18n-util.async.ts +++ /dev/null @@ -1,42 +0,0 @@ -// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. -/* eslint-disable */ - -import { initFormatters } from './formatters' -import type { Locales, Namespaces, Translations } from './i18n-types' -import { loadedFormatters, loadedLocales, locales } from './i18n-util' - -const localeTranslationLoaders = { - en: () => import('./en'), - nb: () => import('./nb'), -} - -const localeNamespaceLoaders = { - en: { - app: () => import('./en/app') - }, - nb: { - app: () => import('./nb/app') - } -} - -const updateDictionary = (locale: Locales, dictionary: Partial<Translations>): Translations => - loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary } - -export const importLocaleAsync = async (locale: Locales): Promise<Translations> => - (await localeTranslationLoaders[locale]()).default as unknown as Translations - -export const loadLocaleAsync = async (locale: Locales): Promise<void> => { - updateDictionary(locale, await importLocaleAsync(locale)) - loadFormatters(locale) -} - -export const loadAllLocalesAsync = (): Promise<void[]> => Promise.all(locales.map(loadLocaleAsync)) - -export const loadFormatters = (locale: Locales): void => - void (loadedFormatters[locale] = initFormatters(locale)) - -export const importNamespaceAsync = async<Namespace extends Namespaces>(locale: Locales, namespace: Namespace) => - (await localeNamespaceLoaders[locale][namespace]()).default as unknown as Translations[Namespace] - -export const loadNamespaceAsync = async <Namespace extends Namespaces>(locale: Locales, namespace: Namespace): Promise<void> => - void updateDictionary(locale, { [namespace]: await importNamespaceAsync(locale, namespace )}) diff --git a/code/app/src/lib/i18n/i18n-util.sync.ts b/code/app/src/lib/i18n/i18n-util.sync.ts deleted file mode 100644 index 8144fdc..0000000 --- a/code/app/src/lib/i18n/i18n-util.sync.ts +++ /dev/null @@ -1,35 +0,0 @@ -// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. -/* eslint-disable */ - -import { initFormatters } from './formatters' -import type { Locales, Translations } from './i18n-types' -import { loadedFormatters, loadedLocales, locales } from './i18n-util' - -import en from './en' -import nb from './nb' - -import en_app from './en/app' -import nb_app from './nb/app' - -const localeTranslations = { - en: { - ...en, - app: en_app - }, - nb: { - ...nb, - app: nb_app - }, -} - -export const loadLocale = (locale: Locales): void => { - if (loadedLocales[locale]) return - - loadedLocales[locale] = localeTranslations[locale] as unknown as Translations - loadFormatters(locale) -} - -export const loadAllLocales = (): void => locales.forEach(loadLocale) - -export const loadFormatters = (locale: Locales): void => - void (loadedFormatters[locale] = initFormatters(locale)) diff --git a/code/app/src/lib/i18n/i18n-util.ts b/code/app/src/lib/i18n/i18n-util.ts deleted file mode 100644 index 12feb33..0000000 --- a/code/app/src/lib/i18n/i18n-util.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. -/* eslint-disable */ - -import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n' -import type { LocaleDetector } from 'typesafe-i18n/detectors' -import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n' -import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors' -import type { Formatters, Locales, Namespaces, Translations, TranslationFunctions } from './i18n-types' - -export const baseLocale: Locales = 'en' - -export const locales: Locales[] = [ - 'en', - 'nb' -] - -export const namespaces: Namespaces[] = [ - 'app' -] - -export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales) - -export const isNamespace = (namespace: string): namespace is Namespaces => namespaces.includes(namespace as Namespaces) - -export const loadedLocales: Record<Locales, Translations> = {} as Record<Locales, Translations> - -export const loadedFormatters: Record<Locales, Formatters> = {} as Record<Locales, Formatters> - -export const i18nString = (locale: Locales): TranslateByString => initI18nString<Locales, Formatters>(locale, loadedFormatters[locale]) - -export const i18nObject = (locale: Locales): TranslationFunctions => - initI18nObject<Locales, Translations, TranslationFunctions, Formatters>( - locale, - loadedLocales[locale], - loadedFormatters[locale] - ) - -export const i18n = (): LocaleTranslationFunctions<Locales, Translations, TranslationFunctions> => - initI18n<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters) - -export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn<Locales>(baseLocale, locales, ...detectors) diff --git a/code/app/src/lib/i18n/nb/app/index.ts b/code/app/src/lib/i18n/nb/app/index.ts deleted file mode 100644 index 6bf9ba6..0000000 --- a/code/app/src/lib/i18n/nb/app/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NamespaceAppTranslation } from '../../i18n-types' - -const nb_app: NamespaceAppTranslation = { - members: "Medlemmer" -} - -export default nb_app diff --git a/code/app/src/lib/i18n/nb/index.ts b/code/app/src/lib/i18n/nb/index.ts deleted file mode 100644 index ef67504..0000000 --- a/code/app/src/lib/i18n/nb/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Translation } from "../i18n-types"; - -const nb: Translation = { - or: "Eller", - name: "Navn", - emailAddress: "E-postadresse", - password: "Passord", - pageNotFound: "Fant ikke siden", - noInternet: "Det ser ut som at du ikke tilkoblet internettet, sjekk tilkoblingen din for å fortsette", - reset: "Tilbakestill", - of: "{0} av {1}", - isRequired: "{0} er påkrevd", - submit: "Send", - success: "Suksess", - tryAgainSoon: "Prøv igjen snart", - createANewAccount: "Lag en ny konto", - unexpectedError: "En uventet feil oppstod", - notFound: "Ikke funnet", - documentation: "Dokumentasjon", - tos: "Vilkår", - privacyPolicy: "Personvernerklæring", - signIntoYourAccount: "Logg inn med din konto", - signInPage: { - notMyComputer: "Dette er ikke min datamaskin", - resetPassword: "Tilbakestill passord", - yourPasswordIsUpdated: "Ditt passord er oppdater", - signIn: "Logg inn", - yourNewPasswordIsApplied: "Ditt nye passord er satt", - signInBelow: "Logg inn nedenfor", - yourAccountIsDisabled: "Din konto er deaktivert", - contactYourAdminIfDisabled: "Ta kontakt med din administrator hvis dette føles feil", - youHaveReachedInactivityLimit: "Du har nådd den hemmelige inaktivitetsgrensen", - feelFreeToSignInAgain: "Logg gjerne inn igjen" - }, - signUpPage: { - createYourNewAccount: "Opprett din nye konto", - }, - resetPasswordPage: { - setANewPassword: "Skriv et nytt passord", - expired: "Utgått", - requestHasExpired: "Din forespørsel er utgått", - requestANewReset: "Spør om en ny tilbakestillingslenke", - newPassword: "Nytt passord", - requestSentMessage: "Hvis vi finner e-postadressen din i våre systemer, vil du få en e-post med instrukser for å sette ditt nye passord.", - requestAPasswordReset: "Forespør tilbakestilling av ditt passord", - requestNotFound: "Din forespørsel ble ikke funnet", - submitANewRequestBelow: "Spør om en ny tilbakestillingslenke nedenfor" - } -} - -export default nb;
\ No newline at end of file diff --git a/code/app/src/lib/logger.ts b/code/app/src/lib/logger.ts deleted file mode 100644 index 831694c..0000000 --- a/code/app/src/lib/logger.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { browser, dev } from "$app/environment"; -import { StorageKeys } from "$lib/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/lib/models/base/Customer.ts b/code/app/src/lib/models/base/Customer.ts deleted file mode 100644 index e44ebb6..0000000 --- a/code/app/src/lib/models/base/Customer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { CustomerContact } from "./CustomerContact" -import type { User } from "./User" - -export type Customer = { - /** - * Guid id for customer - */ - id: string, - /** - * The name of the company - */ - name: string, - /** - * Responsible contact in the current tenant - */ - tenantContact: User, - /** - * The customers main contact - */ - mainContact: CustomerContact, -}
\ No newline at end of file diff --git a/code/app/src/lib/models/base/CustomerContact.ts b/code/app/src/lib/models/base/CustomerContact.ts deleted file mode 100644 index e8abea5..0000000 --- a/code/app/src/lib/models/base/CustomerContact.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type CustomerContact = { - firstName: string, - lastname: string, - email: string, - phone: string, - workTitle: string, - note: string -}
\ No newline at end of file diff --git a/code/app/src/lib/models/base/CustomerEvent.ts b/code/app/src/lib/models/base/CustomerEvent.ts deleted file mode 100644 index af86511..0000000 --- a/code/app/src/lib/models/base/CustomerEvent.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type CustomerEvent = { - /** - * A descriptive name for the occured event - */ - name: string, -}
\ No newline at end of file diff --git a/code/app/src/lib/models/base/SessionData.ts b/code/app/src/lib/models/base/SessionData.ts deleted file mode 100644 index 015cbf3..0000000 --- a/code/app/src/lib/models/base/SessionData.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type SessionData = { - id: string, - username: string, - displayName: string, -}
\ No newline at end of file diff --git a/code/app/src/lib/models/base/Tenant.ts b/code/app/src/lib/models/base/Tenant.ts deleted file mode 100644 index 983122b..0000000 --- a/code/app/src/lib/models/base/Tenant.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { User } from "./User" - -export type Tenant = { - id: string, - name: string, - description: string, - masterUser: User, -}
\ No newline at end of file diff --git a/code/app/src/lib/models/base/User.ts b/code/app/src/lib/models/base/User.ts deleted file mode 100644 index 371c38e..0000000 --- a/code/app/src/lib/models/base/User.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UserRole } from "./UserRole" - -export type User = { - /** - * Guid id for user - */ - id: string, - firstName: string, - lastName: string, - role: UserRole, - username: string, - email: string -}
\ No newline at end of file diff --git a/code/app/src/lib/models/base/UserRole.ts b/code/app/src/lib/models/base/UserRole.ts deleted file mode 100644 index ec32852..0000000 --- a/code/app/src/lib/models/base/UserRole.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum UserRole { - REGULAR = "reg", - ADMINISTRATOR = "adm", - OWNER = "own" -}
\ No newline at end of file diff --git a/code/app/src/lib/models/internal/FormError.ts b/code/app/src/lib/models/internal/FormError.ts deleted file mode 100644 index f6d8978..0000000 --- a/code/app/src/lib/models/internal/FormError.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { KnownProblem } from "./KnownProblem"; - -export class FormError { - title: string; - subtitle: string; - constructor(title: string = "", subtitle: string = "") { - this.title = title; - this.title = subtitle; - } - - set(title: string = "", subtitle: string = "") { - this.title = title; - this.subtitle = subtitle; - } - - set_from_known_problem(knownProblem: KnownProblem) { - this.title = knownProblem.title ?? ""; - this.subtitle = knownProblem.subtitle ?? ""; - } - - has_error() { - return this.title?.length > 0 || this.subtitle?.length > 0; - } -}
\ No newline at end of file diff --git a/code/app/src/lib/models/internal/ISession.ts b/code/app/src/lib/models/internal/ISession.ts deleted file mode 100644 index a452e20..0000000 --- a/code/app/src/lib/models/internal/ISession.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Session = { - profile: { - username: string, - displayName: string, - id: string, - }, - lastChecked: number, -}
\ No newline at end of file diff --git a/code/app/src/lib/models/internal/KnownProblem.ts b/code/app/src/lib/models/internal/KnownProblem.ts deleted file mode 100644 index b6923d9..0000000 --- a/code/app/src/lib/models/internal/KnownProblem.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type KnownProblem = { - title: string, - subtitle: string, - errors: Record<string, string[]>, - traceId: string, -} - -export function is_known_problem(response: Response): boolean { - return response.headers.has("X-IsKnownProblem"); -}
\ No newline at end of file diff --git a/code/app/src/lib/models/projects/Project.ts b/code/app/src/lib/models/projects/Project.ts deleted file mode 100644 index f265e67..0000000 --- a/code/app/src/lib/models/projects/Project.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Temporal } from "temporal-polyfill" -import type { ProjectMember } from "./ProjectMember" -import type { ProjectStatus } from "./ProjectStatus" - -export type Project = { - id: string, - name: string, - description?: string, - start: Temporal.PlainDate, - stop?: Temporal.PlainDate, - members: Array<ProjectMember>, - status: ProjectStatus -}
\ No newline at end of file diff --git a/code/app/src/lib/models/projects/ProjectLabel.ts b/code/app/src/lib/models/projects/ProjectLabel.ts deleted file mode 100644 index 59aa9d5..0000000 --- a/code/app/src/lib/models/projects/ProjectLabel.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ProjectLabel = { - id: string, - name: string, - color: string -}
\ No newline at end of file diff --git a/code/app/src/lib/models/projects/ProjectMember.ts b/code/app/src/lib/models/projects/ProjectMember.ts deleted file mode 100644 index de348ef..0000000 --- a/code/app/src/lib/models/projects/ProjectMember.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ProjectRole } from "./ProjectRole" - -export type ProjectMember = { - id: string, - name: string, - role: ProjectRole, - email: string, - userId?: string, - customerId?: string -}
\ No newline at end of file diff --git a/code/app/src/lib/models/projects/ProjectMeta.ts b/code/app/src/lib/models/projects/ProjectMeta.ts deleted file mode 100644 index c583b47..0000000 --- a/code/app/src/lib/models/projects/ProjectMeta.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Temporal } from "temporal-polyfill" -import type { User } from "../base/User" - -export type ProjectMeta = { - created: Temporal.PlainDateTime, - createdBy: User, -}
\ No newline at end of file diff --git a/code/app/src/lib/models/projects/ProjectRole.ts b/code/app/src/lib/models/projects/ProjectRole.ts deleted file mode 100644 index 0fa2347..0000000 --- a/code/app/src/lib/models/projects/ProjectRole.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ProjectRole { - EXTERNAL = "ext", - INTERNAL = "int", - RESOURCE = "res", - MANAGER = "man", - OWNER = "own" -}
\ No newline at end of file diff --git a/code/app/src/lib/models/projects/ProjectStatus.ts b/code/app/src/lib/models/projects/ProjectStatus.ts deleted file mode 100644 index 2df4b88..0000000 --- a/code/app/src/lib/models/projects/ProjectStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum ProjectStatus { - ACTIVE = "act", - EXPIRED = "exp", - IDLE = "idl" -}
\ No newline at end of file diff --git a/code/app/src/lib/models/work/WorkCategory.ts b/code/app/src/lib/models/work/WorkCategory.ts deleted file mode 100644 index 7dd85d5..0000000 --- a/code/app/src/lib/models/work/WorkCategory.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type WorkCategory = { - id: string, - name: string, - color: string -} diff --git a/code/app/src/lib/models/work/WorkEntry.ts b/code/app/src/lib/models/work/WorkEntry.ts deleted file mode 100644 index 2108b88..0000000 --- a/code/app/src/lib/models/work/WorkEntry.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { WorkLabel } from "./WorkLabel"; -import type { WorkCategory } from "./WorkCategory"; -import type { Project } from "../projects/Project"; - -export type WorkEntry = { - id: string, - start: string, - stop: string, - description: string, - labels?: Array<WorkLabel>, - category?: WorkCategory, - project?: Project -} diff --git a/code/app/src/lib/models/work/WorkEntryQueryResponse.ts b/code/app/src/lib/models/work/WorkEntryQueryResponse.ts deleted file mode 100644 index a6974f1..0000000 --- a/code/app/src/lib/models/work/WorkEntryQueryResponse.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { WorkCategory } from "./WorkCategory"; -import type { WorkLabel } from "./WorkLabel"; -import type { Temporal } from "temporal-polyfill"; - -export interface WorkEntryQueryResponse { - duration: WorkEntryQueryDuration, - categories?: Array<WorkCategory>, - labels?: Array<WorkLabel>, - dateRange?: WorkEntryQueryDateRange, - specificDate?: Temporal.PlainDateTime - page: number, - pageSize: number -} - -export interface WorkEntryQueryDateRange { - from: Temporal.PlainDateTime, - to: Temporal.PlainDateTime -} - -export enum WorkEntryQueryDuration { - TODAY = 0, - THIS_WEEK = 1, - THIS_MONTH = 2, - THIS_YEAR = 3, - SPECIFIC_DATE = 4, - DATE_RANGE = 5, -} diff --git a/code/app/src/lib/models/work/WorkLabel.ts b/code/app/src/lib/models/work/WorkLabel.ts deleted file mode 100644 index f7e2795..0000000 --- a/code/app/src/lib/models/work/WorkLabel.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface WorkLabel { - id?: string, - name?: string, - color?: string -} diff --git a/code/app/src/lib/models/work/WorkQuery.ts b/code/app/src/lib/models/work/WorkQuery.ts deleted file mode 100644 index bccc589..0000000 --- a/code/app/src/lib/models/work/WorkQuery.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { HourEntry } from "./WorkEntry"; -import type { IValidationResult } from "../internal/IValidationResult"; -import ValidationResult from "../internal/IValidationResult"; - -export interface IWorkQuery { - results: Array<HourEntry>, - page: number, - pageSize: number, - totalRecords: number, - totalPageCount: number, - is_valid: Function -} - -export class WorkQuery implements IWorkQuery { - results: HourEntry[]; - page: number; - pageSize: number; - totalRecords: number; - totalPageCount: number; - - is_valid(): IValidationResult { - const result = new ValidationResult(); - if (this.page < 0) { - result.add_error("page", { - title: "Page cannot be less than zero", - }) - } - return result; - } -} diff --git a/code/app/src/lib/persistent-store.ts b/code/app/src/lib/persistent-store.ts deleted file mode 100644 index 922f3ab..0000000 --- a/code/app/src/lib/persistent-store.ts +++ /dev/null @@ -1,102 +0,0 @@ -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 -}; - diff --git a/code/app/src/lib/services/abstractions/IAccountService.ts b/code/app/src/lib/services/abstractions/IAccountService.ts deleted file mode 100644 index 2beeb08..0000000 --- a/code/app/src/lib/services/abstractions/IAccountService.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { KnownProblem } from "$lib/models/internal/KnownProblem" - -export interface IAccountService { - session: Session, - login_async(payload: LoginPayload): Promise<LoginResponse>, - logout_async(): Promise<void>, - create_account_async(payload: CreateAccountPayload): Promise<CreateAccountResponse>, - delete_current_async(): Promise<DeleteAccountResponse>, - update_current_async(payload: UpdateAccountPayload): Promise<UpdateAccountResponse>, -} - -export type Session = { - profile: { - username: string, - displayName: string, - id: string, - }, - lastChecked: number, -} - -export type LoginPayload = { - username: string, - password: string, - persist: boolean -} - -export type LoginResponse = { - isLoggedIn: boolean, - knownProblem?: KnownProblem -} - -export type CreateAccountPayload = { - username: string, - password: string, -} - -export type CreateAccountResponse = { - isCreated: boolean, - knownProblem?: KnownProblem -} - -export type DeleteAccountResponse = { - isDeleted: boolean -} - -export type UpdateAccountPayload = { - username: string, - password: string -} - -export type UpdateAccountResponse = { - isUpdated: boolean, - knownProblem?: KnownProblem -}
\ No newline at end of file diff --git a/code/app/src/lib/services/abstractions/IPasswordResetService.ts b/code/app/src/lib/services/abstractions/IPasswordResetService.ts deleted file mode 100644 index b6f6671..0000000 --- a/code/app/src/lib/services/abstractions/IPasswordResetService.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { KnownProblem } from "$lib/models/internal/KnownProblem" - -export interface IPasswordResetService { - create_request_async(email: string): Promise<CreateRequestResponse>, - fulfill_request_async(id: string, newPassword: string): Promise<FulfillRequestResponse>, - request_is_valid_async(id: string): Promise<RequestIsValidResponse> -} - -export type RequestIsValidResponse = { - isValid: boolean -} - -export type FulfillRequestResponse = { - isFulfilled: boolean, - knownProblem?: KnownProblem -} - -export type CreateRequestResponse = { - isCreated: boolean, - knownProblem?: KnownProblem -}
\ No newline at end of file diff --git a/code/app/src/lib/services/abstractions/ISettingsService.ts b/code/app/src/lib/services/abstractions/ISettingsService.ts deleted file mode 100644 index 366e337..0000000 --- a/code/app/src/lib/services/abstractions/ISettingsService.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ISettingsService { - get_user_settings(): Promise<void>, -}
\ No newline at end of file diff --git a/code/app/src/lib/services/account-service.ts b/code/app/src/lib/services/account-service.ts deleted file mode 100644 index dedf39e..0000000 --- a/code/app/src/lib/services/account-service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { http_delete_async, http_get_async, http_post_async } from "$lib/api/_fetch"; -import { api_base, CookieNames } from "$lib/configuration"; -import { is_known_problem } from "$lib/models/internal/KnownProblem"; -import type { CreateAccountPayload, CreateAccountResponse, DeleteAccountResponse, IAccountService, LoginPayload, LoginResponse, Session, UpdateAccountPayload, UpdateAccountResponse } from "./abstractions/IAccountService"; - -export class AccountService implements IAccountService { - session: Session; - async login_async(payload: LoginPayload): Promise<LoginResponse> { - const response = await http_post_async(api_base("_/account/login"), payload); - if (response.ok) return { isLoggedIn: true }; - if (is_known_problem(response)) return { - isLoggedIn: false, - knownProblem: await response.json() - }; - return { - isLoggedIn: false - } - } - async logout_async(): Promise<void> { - const response = await http_get_async(api_base("_/account/logout")); - - if (!response.ok) { - const deleteCookieResponse = await fetch("/delete-cookie?key=" + CookieNames.session); - if (!deleteCookieResponse.ok) { - throw new Error("Could neither logout nor delete session cookie."); - } - } - - return; - } - async create_account_async(payload: CreateAccountPayload): Promise<CreateAccountResponse> { - const response = await http_post_async(api_base("_/account/create"), payload); - if (response.ok) return { isCreated: true }; - if (is_known_problem(response)) return { - isCreated: false, - knownProblem: await response.json() - } - - return { - isCreated: false - } - } - async delete_current_async(): Promise<DeleteAccountResponse> { - const response = await http_delete_async(api_base("_/account/delete")); - return { - isDeleted: response.ok - } - } - async update_current_async(payload: UpdateAccountPayload): Promise<UpdateAccountResponse> { - const response = await http_post_async(api_base("_/account/update"), payload); - if (response.ok) return { isUpdated: true }; - if (is_known_problem(response)) return { - isUpdated: false, - knownProblem: await response.json() - } - - return { - isUpdated: false - } - } -}
\ No newline at end of file diff --git a/code/app/src/lib/services/password-reset-service.ts b/code/app/src/lib/services/password-reset-service.ts deleted file mode 100644 index 650b5f7..0000000 --- a/code/app/src/lib/services/password-reset-service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { http_get_async, http_post_async } from "$lib/api/_fetch"; -import { api_base } from "$lib/configuration"; -import { is_known_problem } from "$lib/models/internal/KnownProblem"; -import type { CreateRequestResponse, FulfillRequestResponse, IPasswordResetService, RequestIsValidResponse } from "./abstractions/IPasswordResetService"; - -export class PasswordResetService implements IPasswordResetService { - async create_request_async(email: string): Promise<CreateRequestResponse> { - const response = await http_post_async(api_base("_/password-reset-request/create"), { email }); - if (response.ok) return { isCreated: true }; - if (is_known_problem(response)) return { - isCreated: false, - knownProblem: await response.json() - } - - return { - isCreated: false - } - } - async fulfill_request_async(id: string, newPassword: string): Promise<FulfillRequestResponse> { - const response = await http_post_async(api_base("_/password-reset-request/fulfill"), { id: id, newPassword }); - if (response.ok) return { isFulfilled: true }; - if (is_known_problem(response)) return { - isFulfilled: false, - knownProblem: await response.json() - } - - return { - isFulfilled: false, - } - } - async request_is_valid_async(id: string): Promise<RequestIsValidResponse> { - const response = await http_get_async(api_base("_/password-reset-request/is-valid?id=" + id)); - const responseBody = await response.json() as { isValid: boolean }; - return { - isValid: responseBody.isValid - } - } -}
\ No newline at end of file diff --git a/code/app/src/lib/services/settings-service.ts b/code/app/src/lib/services/settings-service.ts deleted file mode 100644 index 6b801ac..0000000 --- a/code/app/src/lib/services/settings-service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ISettingsService } from "./abstractions/ISettingsService"; - -export class SettingsService implements ISettingsService { - get_user_settings(): Promise<void> { - throw new Error("Method not implemented."); - } -}
\ No newline at end of file diff --git a/code/app/src/lib/session.ts b/code/app/src/lib/session.ts deleted file mode 100644 index 5c29cd6..0000000 --- a/code/app/src/lib/session.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { log_error, log_info } from "$lib/logger"; -import { Temporal } from "temporal-polyfill"; -import { http_account } from "$lib/api/account"; -import { is_guid, session_storage_get_json, session_storage_set_json } from "./helpers"; -import { SECONDS_BETWEEN_SESSION_CHECK, StorageKeys } from "./configuration"; -import type { Session } from "$lib/models/internal/ISession"; - -export async function is_active(forceRefresh: boolean = false): Promise<boolean> { - const nowEpoch = Temporal.Now.instant().epochSeconds; - const data = session_storage_get_json(StorageKeys.session) as Session; - const expiryEpoch = data?.lastChecked + SECONDS_BETWEEN_SESSION_CHECK; - const lastCheckIsStaleOrNone = !is_guid(data?.profile?.id) || (expiryEpoch < nowEpoch); - if (forceRefresh || lastCheckIsStaleOrNone) { - return await call_api(); - } else { - const sessionIsValid = data.profile && is_guid(data.profile.id); - if (!sessionIsValid) { - clear_session_data(); - log_info("Session data is not valid"); - } - return sessionIsValid; - } -} - -export async function end_session(cb: Function): Promise<void> { - await http_account.logout_async(); - clear_session_data(); - if (typeof cb === "function") cb(); -} - -async function call_api(): Promise<boolean> { - log_info("Getting profile data while checking session state"); - try { - const response = await http_account.get_profile_async(true); - if (response.ok) { - const userData = await response.json(); - if (is_guid(userData.id) && userData.username) { - const session = { - profile: userData, - lastChecked: Temporal.Now.instant().epochSeconds - } as Session; - session_storage_set_json(StorageKeys.session, session); - log_info("Successfully got profile data while checking session state"); - return true; - } else { - log_error("Api returned invalid data while getting profile data"); - clear_session_data(); - return false; - } - } else { - log_error("Api returned unsuccessfully while getting profile data"); - clear_session_data(); - return false; - } - } catch (e) { - log_error(e); - clear_session_data(); - return false; - } -} - -export function clear_session_data() { - session_storage_set_json(StorageKeys.session, {}); - log_info("Cleared session data."); -} - -export function get_session_data(): Session { - return session_storage_get_json(StorageKeys.session) as Session; -} |
