summaryrefslogtreecommitdiffstats
path: root/apps/web-shared/src/lib
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2022-06-01 22:10:32 +0200
committerivarlovlie <git@ivarlovlie.no>2022-06-01 22:10:32 +0200
commita640703f2da8815dc26ad1600a6f206be1624379 (patch)
treedbda195fb5783d16487e557e06471cf848b75427 /apps/web-shared/src/lib
downloadgreatoffice-a640703f2da8815dc26ad1600a6f206be1624379.tar.xz
greatoffice-a640703f2da8815dc26ad1600a6f206be1624379.zip
feat: Initial after clean slate
Diffstat (limited to 'apps/web-shared/src/lib')
-rw-r--r--apps/web-shared/src/lib/api/internal-fetch.ts170
-rw-r--r--apps/web-shared/src/lib/api/root.ts6
-rw-r--r--apps/web-shared/src/lib/api/time-entry.ts86
-rw-r--r--apps/web-shared/src/lib/api/user.ts47
-rw-r--r--apps/web-shared/src/lib/colors.ts47
-rw-r--r--apps/web-shared/src/lib/configuration.ts60
-rw-r--r--apps/web-shared/src/lib/helpers.ts489
-rw-r--r--apps/web-shared/src/lib/models/CreateAccountPayload.ts4
-rw-r--r--apps/web-shared/src/lib/models/ErrorResult.ts4
-rw-r--r--apps/web-shared/src/lib/models/IInternalFetchRequest.ts6
-rw-r--r--apps/web-shared/src/lib/models/IInternalFetchResponse.ts6
-rw-r--r--apps/web-shared/src/lib/models/ISession.ts7
-rw-r--r--apps/web-shared/src/lib/models/IValidationResult.ts31
-rw-r--r--apps/web-shared/src/lib/models/LoginPayload.ts4
-rw-r--r--apps/web-shared/src/lib/models/TimeCategoryDto.ts9
-rw-r--r--apps/web-shared/src/lib/models/TimeEntryDto.ts13
-rw-r--r--apps/web-shared/src/lib/models/TimeEntryQuery.ts27
-rw-r--r--apps/web-shared/src/lib/models/TimeLabelDto.ts8
-rw-r--r--apps/web-shared/src/lib/models/TimeQueryDto.ts29
-rw-r--r--apps/web-shared/src/lib/models/UnwrappedEntryDateTime.ts9
-rw-r--r--apps/web-shared/src/lib/models/UpdateProfilePayload.ts4
-rw-r--r--apps/web-shared/src/lib/persistent-store.ts102
-rw-r--r--apps/web-shared/src/lib/session.ts62
23 files changed, 1230 insertions, 0 deletions
diff --git a/apps/web-shared/src/lib/api/internal-fetch.ts b/apps/web-shared/src/lib/api/internal-fetch.ts
new file mode 100644
index 0000000..8659ccb
--- /dev/null
+++ b/apps/web-shared/src/lib/api/internal-fetch.ts
@@ -0,0 +1,170 @@
+import {Temporal} from "@js-temporal/polyfill";
+import {clear_session_data} from "$shared/lib/session";
+import {resolve_references} from "$shared/lib/helpers";
+import {replace} from "svelte-spa-router";
+import type {IInternalFetchResponse} from "$shared/lib/models/IInternalFetchResponse";
+import type {IInternalFetchRequest} from "$shared/lib/models/IInternalFetchRequest";
+
+export async function http_post(url: string, body?: object|string, timeout = -1, skip_401_check = false, abort_signal: AbortSignal = undefined): Promise<IInternalFetchResponse> {
+ const init = {
+ method: "post",
+ } as RequestInit;
+
+ if (abort_signal) {
+ init.signal = abort_signal;
+ }
+
+ if (body) {
+ init.headers = {
+ "Content-Type": "application/json;charset=UTF-8",
+ };
+ init.body = JSON.stringify(body);
+ }
+
+ const response = await internal_fetch({url, init, timeout});
+ const result = {} as IInternalFetchResponse;
+
+ if (!skip_401_check && await is_401(response)) return result;
+
+ result.ok = response.ok;
+ result.status = response.status;
+ result.http_response = response;
+
+ if (response.status !== 204) {
+ try {
+ const ct = response.headers.get("Content-Type")?.toString() ?? "";
+ if (ct.startsWith("application/json")) {
+ const data = await response.json();
+ result.data = resolve_references(data);
+ } else if (ct.startsWith("text/plain")) {
+ const text = await response.text();
+ result.data = text as string;
+ }
+ } catch {
+ // Ignored
+ }
+ }
+
+ return result;
+}
+
+export async function http_get(url: string, timeout = -1, skip_401_check = false, abort_signal: AbortSignal = undefined): Promise<IInternalFetchResponse> {
+ const init = {
+ method: "get",
+ } as RequestInit;
+
+ if (abort_signal) {
+ init.signal = abort_signal;
+ }
+
+ const response = await internal_fetch({url, init, timeout});
+ const result = {} as IInternalFetchResponse;
+
+ if (!skip_401_check && await is_401(response)) return result;
+
+ result.ok = response.ok;
+ result.status = response.status;
+ result.http_response = response;
+
+ if (response.status !== 204) {
+ try {
+ const ct = response.headers.get("Content-Type")?.toString() ?? "";
+ if (ct.startsWith("application/json")) {
+ const data = await response.json();
+ result.data = resolve_references(data);
+ } else if (ct.startsWith("text/plain")) {
+ const text = await response.text();
+ result.data = text as string;
+ }
+ } catch {
+ // Ignored
+ }
+ }
+
+ return result;
+}
+
+export async function http_delete(url: string, body?: object|string, timeout = -1, skip_401_check = false, abort_signal: AbortSignal = undefined): Promise<IInternalFetchResponse> {
+ const init = {
+ method: "delete",
+ } as RequestInit;
+
+ if (abort_signal) {
+ init.signal = abort_signal;
+ }
+
+ if (body) {
+ init.headers = {
+ "Content-Type": "application/json;charset=UTF-8",
+ };
+ init.body = JSON.stringify(body);
+ }
+
+ const response = await internal_fetch({url, init, timeout});
+ const result = {} as IInternalFetchResponse;
+
+ if (!skip_401_check && await is_401(response)) return result;
+
+ result.ok = response.ok;
+ result.status = response.status;
+ result.http_response = response;
+
+ if (response.status !== 204) {
+ try {
+ const ct = response.headers.get("Content-Type")?.toString() ?? "";
+ if (ct.startsWith("application/json")) {
+ const data = await response.json();
+ result.data = resolve_references(data);
+ } else if (ct.startsWith("text/plain")) {
+ const text = await response.text();
+ result.data = text as string;
+ }
+ } catch (error) {
+ // ignored
+ }
+ }
+
+ return result;
+}
+
+async function internal_fetch(request: IInternalFetchRequest): Promise<Response> {
+ if (!request.init) request.init = {};
+ request.init.credentials = "include";
+ request.init.headers = {
+ "X-TimeZone": Temporal.Now.timeZone().id,
+ ...request.init.headers
+ };
+
+ const fetch_request = new Request(request.url, request.init);
+ let response: any;
+
+ try {
+ if (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) {
+ if (error.message === "Timeout") {
+ console.error("Request timed out");
+ } else if (error.message === "Network request failed") {
+ console.error("No internet connection");
+ } else {
+ throw error; // rethrow other unexpected errors
+ }
+ }
+
+ return response;
+}
+
+async function is_401(response: Response): Promise<boolean> {
+ if (response.status === 401) {
+ clear_session_data();
+ await replace("/login");
+ return true;
+ }
+ return false;
+}
diff --git a/apps/web-shared/src/lib/api/root.ts b/apps/web-shared/src/lib/api/root.ts
new file mode 100644
index 0000000..d65efc4
--- /dev/null
+++ b/apps/web-shared/src/lib/api/root.ts
@@ -0,0 +1,6 @@
+import {http_post} from "$shared/lib/api/internal-fetch";
+import {api_base} from "$shared/lib/configuration";
+
+export function server_log(message: string): void {
+ http_post(api_base("_/api/log"), message);
+}
diff --git a/apps/web-shared/src/lib/api/time-entry.ts b/apps/web-shared/src/lib/api/time-entry.ts
new file mode 100644
index 0000000..e81329d
--- /dev/null
+++ b/apps/web-shared/src/lib/api/time-entry.ts
@@ -0,0 +1,86 @@
+import {api_base} from "$shared/lib/configuration";
+import {is_guid} from "$shared/lib/helpers";
+import {http_delete, http_get, http_post} from "./internal-fetch";
+import type {TimeCategoryDto} from "$shared/lib/models/TimeCategoryDto";
+import type {TimeLabelDto} from "$shared/lib/models/TimeLabelDto";
+import type {TimeEntryDto} from "$shared/lib/models/TimeEntryDto";
+import type {TimeEntryQuery} from "$shared/lib/models/TimeEntryQuery";
+import type {IInternalFetchResponse} from "$shared/lib/models/IInternalFetchResponse";
+
+
+// ENTRIES
+
+export async function create_time_entry(payload: TimeEntryDto): Promise<IInternalFetchResponse> {
+ return http_post(api_base("v1/entries/create"), payload);
+}
+
+export async function get_time_entry(entryId: string): Promise<IInternalFetchResponse> {
+ if (is_guid(entryId)) {
+ return http_get(api_base("v1/entries/" + entryId));
+ }
+ throw new Error("entryId is not a valid guid.");
+}
+
+export async function get_time_entries(entryQuery: TimeEntryQuery): Promise<IInternalFetchResponse> {
+ return http_post(api_base("v1/entries/query"), entryQuery);
+}
+
+export async function delete_time_entry(id: string): Promise<IInternalFetchResponse> {
+ if (!is_guid(id)) throw new Error("id is not a valid guid");
+ return http_delete(api_base("v1/entries/" + id + "/delete"));
+}
+
+export async function update_time_entry(entryDto: TimeEntryDto): Promise<IInternalFetchResponse> {
+ if (!is_guid(entryDto.id ?? "")) throw new Error("id is not a valid guid");
+ if (!entryDto.category) throw new Error("category is empty");
+ if (!entryDto.stop) throw new Error("stop is empty");
+ if (!entryDto.start) throw new Error("start is empty");
+ return http_post(api_base("v1/entries/update"), entryDto);
+}
+
+// LABELS
+
+
+export async function create_time_label(labelDto: TimeLabelDto): Promise<IInternalFetchResponse> {
+ return http_post(api_base("v1/labels/create"), labelDto);
+}
+
+export async function get_time_labels(): Promise<IInternalFetchResponse> {
+ return http_get(api_base("v1/labels"));
+}
+
+export async function delete_time_label(id: string): Promise<IInternalFetchResponse> {
+ if (!is_guid(id)) throw new Error("id is not a valid guid");
+ return http_delete(api_base("v1/labels/" + id + "/delete"));
+}
+
+export async function update_time_label(labelDto: TimeLabelDto): Promise<IInternalFetchResponse> {
+ if (!is_guid(labelDto.id ?? "")) throw new Error("id is not a valid guid");
+ if (!labelDto.name) throw new Error("name is empty");
+ if (!labelDto.color) throw new Error("color is empty");
+ return http_post(api_base("v1/labels/update"), labelDto);
+}
+
+
+// CATEGORIES
+export async function create_time_category(category: TimeCategoryDto): Promise<IInternalFetchResponse> {
+ if (!category.name) throw new Error("name is empty");
+ if (!category.color) throw new Error("color is empty");
+ return http_post(api_base("v1/categories/create"), category);
+}
+
+export async function get_time_categories(): Promise<IInternalFetchResponse> {
+ return http_get(api_base("v1/categories"));
+}
+
+export async function delete_time_category(id: string): Promise<IInternalFetchResponse> {
+ if (!is_guid(id)) throw new Error("id is not a valid guid");
+ return http_delete(api_base("v1/categories/" + id + "/delete"));
+}
+
+export async function update_time_category(category: TimeCategoryDto): Promise<IInternalFetchResponse> {
+ if (!is_guid(category.id ?? "")) throw new Error("id is not a valid guid");
+ if (!category.name) throw new Error("name is empty");
+ if (!category.color) throw new Error("color is empty");
+ return http_post(api_base("v1/categories/update"), category);
+}
diff --git a/apps/web-shared/src/lib/api/user.ts b/apps/web-shared/src/lib/api/user.ts
new file mode 100644
index 0000000..a3a149e
--- /dev/null
+++ b/apps/web-shared/src/lib/api/user.ts
@@ -0,0 +1,47 @@
+import {api_base} from "$shared/lib/configuration";
+import {http_delete, http_get, http_post} from "./internal-fetch";
+import type {LoginPayload} from "$shared/lib/models/LoginPayload";
+import type {UpdateProfilePayload} from "$shared/lib/models/UpdateProfilePayload";
+import type {CreateAccountPayload} from "$shared/lib/models/CreateAccountPayload";
+import type {IInternalFetchResponse} from "$shared/lib/models/IInternalFetchResponse";
+
+export async function login(payload: LoginPayload): Promise<IInternalFetchResponse> {
+ return http_post(api_base("_/account/login"), payload);
+}
+
+export async function logout(): Promise<IInternalFetchResponse> {
+ return http_get(api_base("_/account/logout"));
+}
+
+export async function create_forgot_password_request(username: string): Promise<IInternalFetchResponse> {
+ if (!username) throw new Error("Username is empty");
+ return http_get(api_base("_/forgot-password-requests/create?username=" + username));
+}
+
+export async function check_forgot_password_request(public_id: string): Promise<IInternalFetchResponse> {
+ if (!public_id) throw new Error("Id is empty");
+ return http_get(api_base("_/forgot-password-requests/is-valid?id=" + public_id));
+}
+
+export async function fulfill_forgot_password_request(public_id: string, newPassword: string): Promise<IInternalFetchResponse> {
+ if (!public_id) throw new Error("Id is empty");
+ return http_post(api_base("_/forgot-password-requests/fulfill"), {id: public_id, newPassword});
+}
+
+export async function delete_account(): Promise<IInternalFetchResponse> {
+ return http_delete(api_base("_/account/delete"));
+}
+
+export async function update_profile(payload: UpdateProfilePayload): Promise<IInternalFetchResponse> {
+ if (!payload.password && !payload.username) throw new Error("Password and Username is empty");
+ return http_post(api_base("_/account/update"), payload);
+}
+
+export async function create_account(payload: CreateAccountPayload): Promise<IInternalFetchResponse> {
+ if (!payload.password && !payload.username) throw new Error("Password and Username is empty");
+ return http_post(api_base("_/account/create"), payload);
+}
+
+export async function get_profile_for_active_check(): Promise<IInternalFetchResponse> {
+ return http_get(api_base("_/account"), 0, true);
+}
diff --git a/apps/web-shared/src/lib/colors.ts b/apps/web-shared/src/lib/colors.ts
new file mode 100644
index 0000000..c2da03d
--- /dev/null
+++ b/apps/web-shared/src/lib/colors.ts
@@ -0,0 +1,47 @@
+export function generate_random_hex_color(skip_contrast_check = false) {
+ let hex = __generate_random_hex_color();
+ if (skip_contrast_check) return hex;
+ while ((__calculate_contrast_ratio("#ffffff", hex) < 4.5) || (__calculate_contrast_ratio("#000000", hex) < 4.5)) {
+ hex = __generate_random_hex_color();
+ }
+
+ return hex;
+}
+
+// Largely copied from chroma js api
+function __generate_random_hex_color(): string {
+ let code = "#";
+ for (let i = 0; i < 6; i++) {
+ code += "0123456789abcdef".charAt(Math.floor(Math.random() * 16));
+ }
+ return code;
+}
+
+function __calculate_contrast_ratio(hex1: string, hex2: string): number {
+ const rgb1 = __hex_to_rgb(hex1);
+ const rgb2 = __hex_to_rgb(hex2);
+ const l1 = __get_luminance(rgb1[0], rgb1[1], rgb1[2]);
+ const l2 = __get_luminance(rgb2[0], rgb2[1], rgb2[2]);
+ const result = l1 > l2 ? (l1 + 0.05) / (l2 + 0.05) : (l2 + 0.05) / (l1 + 0.05);
+ return result;
+}
+
+function __hex_to_rgb(hex: string): number[] {
+ if (!hex.match(/^#([A-Fa-f0-9]{6})$/)) return [];
+ if (hex[0] === "#") hex = hex.substring(1, hex.length);
+ return [parseInt(hex.substring(0, 2), 16), parseInt(hex.substring(2, 4), 16), parseInt(hex.substring(4, 6), 16)];
+}
+
+function __get_luminance(r, g, b) {
+ // 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) {
+ x /= 255;
+ return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
+}
diff --git a/apps/web-shared/src/lib/configuration.ts b/apps/web-shared/src/lib/configuration.ts
new file mode 100644
index 0000000..f597bb4
--- /dev/null
+++ b/apps/web-shared/src/lib/configuration.ts
@@ -0,0 +1,60 @@
+export const API_ADDRESS = "https://api.dev.greatoffice.life";
+export const PROJECTS_ADDRESS = "https://projects.dev.greatoffice.life";
+export const ACCOUNTS_ADDRESS = "https://a.dev.greatoffice.life";
+export const FRONTPAGE_ADDRESS = "https://greatoffice.life";
+export const DEV_ACCOUNTS_ADDRESS = "http://localhost:3001";
+export const DEV_FRONTPAGE_ADDRESS = "http://localhost:3002";
+export const DEV_API_ADDRESS = "http://localhost:5000";
+export const DEV_PROJECTS_ADDRESS = "http://localhost:3000";
+export const SECONDS_BETWEEN_SESSION_CHECK = 600;
+
+export function projects_base(path: string): string {
+ return (is_development() ? DEV_PROJECTS_ADDRESS : PROJECTS_ADDRESS) + (path ? "/" + path : "/");
+}
+
+export function frontpage_base(path: string): string {
+ return (is_development() ? DEV_FRONTPAGE_ADDRESS : FRONTPAGE_ADDRESS) + (path ? "/" + path : "/");
+}
+
+export function accounts_base(path: string): string {
+ return (is_development() ? DEV_ACCOUNTS_ADDRESS : ACCOUNTS_ADDRESS) + (path ? "/" + path : "/");
+}
+
+export function api_base(path: string): string {
+ return (is_development() ? DEV_API_ADDRESS : API_ADDRESS) + (path ? "/" + path : "/");
+}
+
+export function is_development(): boolean {
+ // @ts-ignore
+ return import.meta.env.DEV;
+}
+
+export function is_debug(): boolean {
+ return localStorage.getItem(StorageKeys.debug) !== "true";
+}
+
+export const IconNames = {
+ github: "github",
+ verticalDots: "verticalDots",
+ clock: "clock",
+ trash: "trash",
+ pencilSquare: "pencilSquare",
+ x: "x",
+ funnel: "funnel",
+ funnelFilled: "funnelFilled",
+ refresh: "refresh",
+ resetHard: "resetHard",
+ arrowUp: "arrowUp",
+ arrowDown: "arrowDown",
+ chevronDown: "chevronDown"
+};
+
+export const StorageKeys = {
+ session: "sessionData",
+ theme: "theme",
+ debug: "debug",
+ categories: "categories",
+ labels: "labels",
+ entries: "entries",
+ stopwatch: "stopwatchState"
+};
diff --git a/apps/web-shared/src/lib/helpers.ts b/apps/web-shared/src/lib/helpers.ts
new file mode 100644
index 0000000..650bccf
--- /dev/null
+++ b/apps/web-shared/src/lib/helpers.ts
@@ -0,0 +1,489 @@
+import {StorageKeys} from "$shared/lib/configuration";
+import {TimeEntryDto} from "$shared/lib/models/TimeEntryDto";
+import {UnwrappedEntryDateTime} from "$shared/lib/models/UnwrappedEntryDateTime";
+import {Temporal} from "@js-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 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 switch_theme() {
+ const html = document.querySelector("html");
+ if (html) {
+ if (html.dataset.theme === "dark") {
+ html.dataset.theme = "light";
+ } else {
+ html.dataset.theme = "dark";
+ }
+ localStorage.setItem(StorageKeys.theme, html.dataset.theme);
+ }
+}
+
+export function unwrap_date_time_from_entry(entry: TimeEntryDto): UnwrappedEntryDateTime {
+ if (!entry) throw new Error("entry was undefined");
+ const currentTimeZone = Temporal.Now.timeZone().id;
+ const startInstant = Temporal.Instant.from(entry.start).toZonedDateTimeISO(currentTimeZone);
+ const stopInstant = Temporal.Instant.from(entry.stop).toZonedDateTimeISO(currentTimeZone);
+
+ return {
+ start_date: startInstant.toPlainDate(),
+ stop_date: stopInstant.toPlainDate(),
+ start_time: startInstant.toPlainTime(),
+ stop_time: stopInstant.toPlainTime(),
+ duration: Temporal.Duration.from({
+ hours: stopInstant.hour,
+ minutes: stopInstant.minute
+ }).subtract(Temporal.Duration.from({
+ hours: startInstant.hour,
+ minutes: startInstant.minute
+ }))
+ };
+}
+
+
+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 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) {
+ const hours = Math.floor(seconds / (60 * 60));
+ seconds -= hours * (60 * 60);
+ const minutes = Math.floor(seconds / 60);
+ return hours + "h" + minutes + "m";
+}
+
+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 load_script(url: string) {
+ unload_script(url, () => {
+ return new Promise(function (resolve, reject) {
+ const script = document.createElement("script");
+ script.src = url;
+
+ script.addEventListener("load", function () {
+ // The script is loaded completely
+ resolve(true);
+ });
+
+ document.body.appendChild(script);
+ });
+ });
+}
+
+export function unload_script(src: string, callback?: Function): void {
+ document.querySelectorAll("script[src='" + src + "']").forEach(el => el.remove());
+ if (typeof callback === "function") {
+ callback();
+ }
+}
+
+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 uuid_v4(): string {
+ // @ts-ignore
+ return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
+}
+
+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 get_local_time_zone_date(date: Date): Date {
+ const timeOffsetInMS = new Date().getTimezoneOffset() * 60000;
+ date.setTime(date.getTime() - timeOffsetInMS);
+ return date;
+}
+
+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 to_readable_date_string(date: Date, locale = "nb-NO"): string {
+ date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
+ return date.toLocaleString(locale);
+}
+
+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 {
+ 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 {
+ 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 {
+ sessionStorage.setItem(key, JSON.stringify(value));
+}
+
+export function session_storage_get_json(key: string): object {
+ return JSON.parse(sessionStorage.getItem(key) ?? "{}");
+}
+
+export function local_storage_set_json(key: string, value: object): void {
+ localStorage.setItem(key, JSON.stringify(value));
+}
+
+export function local_storage_get_json(key: string): object {
+ 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;
+}
+
+export function $(selector: string): HTMLElement|null {
+ return document.querySelector(selector);
+}
+
+export function $$(selector: string): NodeListOf<Element> {
+ return document.querySelectorAll(selector);
+}
diff --git a/apps/web-shared/src/lib/models/CreateAccountPayload.ts b/apps/web-shared/src/lib/models/CreateAccountPayload.ts
new file mode 100644
index 0000000..d116308
--- /dev/null
+++ b/apps/web-shared/src/lib/models/CreateAccountPayload.ts
@@ -0,0 +1,4 @@
+export interface CreateAccountPayload {
+ username: string,
+ password: string
+}
diff --git a/apps/web-shared/src/lib/models/ErrorResult.ts b/apps/web-shared/src/lib/models/ErrorResult.ts
new file mode 100644
index 0000000..7c70017
--- /dev/null
+++ b/apps/web-shared/src/lib/models/ErrorResult.ts
@@ -0,0 +1,4 @@
+export interface ErrorResult {
+ title: string,
+ text: string
+}
diff --git a/apps/web-shared/src/lib/models/IInternalFetchRequest.ts b/apps/web-shared/src/lib/models/IInternalFetchRequest.ts
new file mode 100644
index 0000000..68505e2
--- /dev/null
+++ b/apps/web-shared/src/lib/models/IInternalFetchRequest.ts
@@ -0,0 +1,6 @@
+export interface IInternalFetchRequest {
+ url: string,
+ init?: RequestInit,
+ timeout?: number
+ retry_count?: number
+}
diff --git a/apps/web-shared/src/lib/models/IInternalFetchResponse.ts b/apps/web-shared/src/lib/models/IInternalFetchResponse.ts
new file mode 100644
index 0000000..6c91b35
--- /dev/null
+++ b/apps/web-shared/src/lib/models/IInternalFetchResponse.ts
@@ -0,0 +1,6 @@
+export interface IInternalFetchResponse {
+ ok: boolean,
+ status: number,
+ data: any,
+ http_response: Response
+}
diff --git a/apps/web-shared/src/lib/models/ISession.ts b/apps/web-shared/src/lib/models/ISession.ts
new file mode 100644
index 0000000..f7ed46b
--- /dev/null
+++ b/apps/web-shared/src/lib/models/ISession.ts
@@ -0,0 +1,7 @@
+export interface ISession {
+ profile: {
+ username: string,
+ id: string,
+ },
+ lastChecked: number,
+} \ No newline at end of file
diff --git a/apps/web-shared/src/lib/models/IValidationResult.ts b/apps/web-shared/src/lib/models/IValidationResult.ts
new file mode 100644
index 0000000..9a21b13
--- /dev/null
+++ b/apps/web-shared/src/lib/models/IValidationResult.ts
@@ -0,0 +1,31 @@
+export interface IValidationResult {
+ errors: Array<IValidationError>,
+ has_errors: Function,
+ add_error: Function,
+ remove_error: Function,
+}
+
+export interface IValidationError {
+ _id?: string,
+ title: string,
+ text?: string
+}
+
+export default class ValidationResult implements IValidationResult {
+ errors: IValidationError[]
+ has_errors(): boolean {
+ return this.errors?.length > 0;
+ }
+ add_error(prop: string, error: IValidationError): void {
+ if (!this.errors) this.errors = [];
+ error._id = prop;
+ this.errors.push(error);
+ }
+ remove_error(property: string): void {
+ const new_errors = [];
+ for (const error of this.errors) {
+ if (error._id != property) new_errors.push(error)
+ }
+ this.errors = new_errors;
+ }
+}
diff --git a/apps/web-shared/src/lib/models/LoginPayload.ts b/apps/web-shared/src/lib/models/LoginPayload.ts
new file mode 100644
index 0000000..ccd9bed
--- /dev/null
+++ b/apps/web-shared/src/lib/models/LoginPayload.ts
@@ -0,0 +1,4 @@
+export interface LoginPayload {
+ username: string,
+ password: string
+}
diff --git a/apps/web-shared/src/lib/models/TimeCategoryDto.ts b/apps/web-shared/src/lib/models/TimeCategoryDto.ts
new file mode 100644
index 0000000..875e8cb
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeCategoryDto.ts
@@ -0,0 +1,9 @@
+import { Temporal } from "@js-temporal/polyfill";
+
+export interface TimeCategoryDto {
+ selected?: boolean;
+ id?: string,
+ modified_at?: Temporal.PlainDate,
+ name?: string,
+ color?: string
+}
diff --git a/apps/web-shared/src/lib/models/TimeEntryDto.ts b/apps/web-shared/src/lib/models/TimeEntryDto.ts
new file mode 100644
index 0000000..71fe7a3
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeEntryDto.ts
@@ -0,0 +1,13 @@
+import type { TimeLabelDto } from "./TimeLabelDto";
+import type { TimeCategoryDto } from "./TimeCategoryDto";
+import { Temporal } from "@js-temporal/polyfill";
+
+export interface TimeEntryDto {
+ id: string,
+ modified_at?: Temporal.PlainDate,
+ start: string,
+ stop: string,
+ description: string,
+ labels?: Array<TimeLabelDto>,
+ category: TimeCategoryDto,
+}
diff --git a/apps/web-shared/src/lib/models/TimeEntryQuery.ts b/apps/web-shared/src/lib/models/TimeEntryQuery.ts
new file mode 100644
index 0000000..6681c79
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeEntryQuery.ts
@@ -0,0 +1,27 @@
+import type { TimeCategoryDto } from "./TimeCategoryDto";
+import type { TimeLabelDto } from "./TimeLabelDto";
+import type { Temporal } from "@js-temporal/polyfill";
+
+export interface TimeEntryQuery {
+ duration: TimeEntryQueryDuration,
+ categories?: Array<TimeCategoryDto>,
+ labels?: Array<TimeLabelDto>,
+ dateRange?: TimeEntryQueryDateRange,
+ specificDate?: Temporal.PlainDateTime
+ page: number,
+ pageSize: number
+}
+
+export interface TimeEntryQueryDateRange {
+ from: Temporal.PlainDateTime,
+ to: Temporal.PlainDateTime
+}
+
+export enum TimeEntryQueryDuration {
+ TODAY = 0,
+ THIS_WEEK = 1,
+ THIS_MONTH = 2,
+ THIS_YEAR = 3,
+ SPECIFIC_DATE = 4,
+ DATE_RANGE = 5,
+}
diff --git a/apps/web-shared/src/lib/models/TimeLabelDto.ts b/apps/web-shared/src/lib/models/TimeLabelDto.ts
new file mode 100644
index 0000000..2b42d07
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeLabelDto.ts
@@ -0,0 +1,8 @@
+import { Temporal } from "@js-temporal/polyfill";
+
+export interface TimeLabelDto {
+ id?: string,
+ modified_at?: Temporal.PlainDate,
+ name?: string,
+ color?: string
+}
diff --git a/apps/web-shared/src/lib/models/TimeQueryDto.ts b/apps/web-shared/src/lib/models/TimeQueryDto.ts
new file mode 100644
index 0000000..607c51e
--- /dev/null
+++ b/apps/web-shared/src/lib/models/TimeQueryDto.ts
@@ -0,0 +1,29 @@
+import type { TimeEntryDto } from "./TimeEntryDto";
+import ValidationResult, { IValidationResult } from "./IValidationResult";
+
+export interface ITimeQueryDto {
+ results: Array<TimeEntryDto>,
+ page: number,
+ pageSize: number,
+ totalRecords: number,
+ totalPageCount: number,
+ is_valid: Function
+}
+
+export class TimeQueryDto implements ITimeQueryDto {
+ results: TimeEntryDto[];
+ 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/apps/web-shared/src/lib/models/UnwrappedEntryDateTime.ts b/apps/web-shared/src/lib/models/UnwrappedEntryDateTime.ts
new file mode 100644
index 0000000..e6022d8
--- /dev/null
+++ b/apps/web-shared/src/lib/models/UnwrappedEntryDateTime.ts
@@ -0,0 +1,9 @@
+import {Temporal} from "@js-temporal/polyfill";
+
+export interface UnwrappedEntryDateTime {
+ start_date: Temporal.PlainDate,
+ stop_date: Temporal.PlainDate,
+ start_time: Temporal.PlainTime,
+ stop_time: Temporal.PlainTime,
+ duration: Temporal.Duration,
+}
diff --git a/apps/web-shared/src/lib/models/UpdateProfilePayload.ts b/apps/web-shared/src/lib/models/UpdateProfilePayload.ts
new file mode 100644
index 0000000..d2983ff
--- /dev/null
+++ b/apps/web-shared/src/lib/models/UpdateProfilePayload.ts
@@ -0,0 +1,4 @@
+export interface UpdateProfilePayload {
+ username?: string,
+ password?: string,
+}
diff --git a/apps/web-shared/src/lib/persistent-store.ts b/apps/web-shared/src/lib/persistent-store.ts
new file mode 100644
index 0000000..922f3ab
--- /dev/null
+++ b/apps/web-shared/src/lib/persistent-store.ts
@@ -0,0 +1,102 @@
+import { writable as _writable, readable as _readable, } from "svelte/store";
+import type { Writable, Readable, StartStopNotifier } from "svelte/store";
+
+enum StoreType {
+ SESSION = 0,
+ LOCAL = 1
+}
+
+interface StoreOptions {
+ store?: StoreType;
+}
+
+const default_store_options = {
+ store: StoreType.SESSION
+} as StoreOptions;
+
+interface WritableStore<T> {
+ name: string,
+ initialState: T,
+ options?: StoreOptions
+}
+
+interface ReadableStore<T> {
+ name: string,
+ initialState: T,
+ callback: StartStopNotifier<any>,
+ options?: StoreOptions
+}
+
+function get_store(type: StoreType): Storage {
+ switch (type) {
+ case StoreType.SESSION:
+ return window.sessionStorage;
+ case StoreType.LOCAL:
+ return window.localStorage;
+ }
+}
+
+function prepared_store_value(value: any): string {
+ try {
+ return JSON.stringify(value);
+ } catch (e) {
+ console.error(e);
+ return "__INVALID__";
+ }
+}
+
+function get_store_value<T>(options: WritableStore<T> | ReadableStore<T>): any {
+ try {
+ const storage = get_store(options.options.store);
+ const value = storage.getItem(options.name);
+ if (!value) return false;
+ return JSON.parse(value);
+ } catch (e) {
+ console.error(e);
+ return { __INVALID__: true };
+ }
+}
+
+function hydrate<T>(store: Writable<T>, options: WritableStore<T> | ReadableStore<T>): void {
+ const value = get_store_value<T>(options);
+ if (value && store.set) store.set(value);
+}
+
+function subscribe<T>(store: Writable<T> | Readable<T>, options: WritableStore<T> | ReadableStore<T>): void {
+ const storage = get_store(options.options.store);
+ if (!store.subscribe) return;
+ store.subscribe((state: any) => {
+ storage.setItem(options.name, prepared_store_value(state));
+ });
+}
+
+function writable_persistent<T>(options: WritableStore<T>): Writable<T> {
+ if (options.options === undefined) options.options = default_store_options;
+ console.log("Creating writable store with options: ", options);
+ const store = _writable<T>(options.initialState);
+ hydrate(store, options);
+ subscribe(store, options);
+ return store;
+}
+
+function readable_persistent<T>(options: ReadableStore<T>): Readable<T> {
+ if (options.options === undefined) options.options = default_store_options;
+ console.log("Creating readable store with options: ", options);
+ const store = _readable<T>(options.initialState, options.callback);
+ // hydrate(store, options);
+ subscribe(store, options);
+ return store;
+}
+
+export {
+ writable_persistent,
+ readable_persistent,
+ StoreType
+};
+
+export type {
+ WritableStore,
+ ReadableStore,
+ StoreOptions
+};
+
diff --git a/apps/web-shared/src/lib/session.ts b/apps/web-shared/src/lib/session.ts
new file mode 100644
index 0000000..4f40a17
--- /dev/null
+++ b/apps/web-shared/src/lib/session.ts
@@ -0,0 +1,62 @@
+import {Temporal} from "@js-temporal/polyfill";
+import {get_profile_for_active_check} from "./api/user";
+import {is_guid, session_storage_get_json, session_storage_set_json} from "./helpers";
+import {SECONDS_BETWEEN_SESSION_CHECK, StorageKeys} from "./configuration";
+import type {ISession} from "$shared/lib/models/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 ISession;
+ 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();
+ console.log("Session data is not valid");
+ }
+ return sessionIsValid;
+ }
+}
+
+async function call_api(): Promise<boolean> {
+ console.log("Getting profile data while checking session state");
+ try {
+ const response = await get_profile_for_active_check();
+ if (response.ok) {
+ const userData = await response.data;
+ if (is_guid(userData.id) && userData.username) {
+ const session = {
+ profile: userData,
+ lastChecked: Temporal.Now.instant().epochSeconds
+ } as ISession;
+ session_storage_set_json(StorageKeys.session, session);
+ console.log("Successfully got profile data while checking session state");
+ return true;
+ } else {
+ console.error("Api returned invalid data while getting profile data");
+ clear_session_data();
+ return false;
+ }
+ } else {
+ console.error("Api returned unsuccessfully while getting profile data");
+ clear_session_data();
+ return false;
+ }
+ } catch (e) {
+ console.error(e);
+ clear_session_data();
+ return false;
+ }
+}
+
+export function clear_session_data() {
+ session_storage_set_json(StorageKeys.session, {});
+ console.log("Cleared session data.");
+}
+
+export function get_session_data(): ISession {
+ return session_storage_get_json(StorageKeys.session) as ISession;
+}