WIP: major refactoring

This commit is contained in:
Jörn-Michael Miehe 2024-08-23 16:38:04 +00:00
parent d8fe5dc9f4
commit 55081b24d8
28 changed files with 262 additions and 296 deletions

View file

@ -34,7 +34,7 @@
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { advent22Store } from "./plugins/store";
import { advent22Store } from "./lib/store";
import AdminView from "./components/admin/AdminView.vue";
import AdminButton from "./components/AdminButton.vue";

View file

@ -11,8 +11,8 @@
</template>
<script lang="ts">
import { Credentials } from "@/lib/api";
import { advent22Store } from "@/plugins/store";
import { Credentials } from "@/lib/model";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
import BulmaButton from "./bulma/Button.vue";

View file

@ -46,8 +46,8 @@
</template>
<script lang="ts">
import { Door } from "@/lib/door";
import { advent22Store } from "@/plugins/store";
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
import MultiModal from "./MultiModal.vue";

View file

@ -12,7 +12,7 @@
</template>
<script lang="ts">
import { advent22Store } from "@/plugins/store";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
import BulmaButton from "./bulma/Button.vue";

View file

@ -23,7 +23,7 @@
</template>
<script lang="ts">
import { advent22Store } from "@/plugins/store";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
import Calendar from "./Calendar.vue";

View file

@ -46,7 +46,8 @@
</template>
<script lang="ts">
import { NumStrDict, objForEach } from "@/lib/api";
import { objForEach } from "@/lib/helpers";
import { NumStrDict } from "@/lib/model";
import { Options, Vue } from "vue-class-component";
import MultiModal from "../MultiModal.vue";

View file

@ -184,8 +184,8 @@
</template>
<script lang="ts">
import { AdminConfigModel, Credentials, DoorSaved } from "@/lib/api";
import { advent22Store } from "@/plugins/store";
import { AdminConfigModel, Credentials, DoorSaved } from "@/lib/model";
import { advent22Store } from "@/lib/store";
import { DateTime } from "luxon";
import { Options, Vue } from "vue-class-component";

View file

@ -69,9 +69,9 @@
</template>
<script lang="ts">
import { DoorSaved } from "@/lib/api";
import { Door } from "@/lib/door";
import { advent22Store } from "@/plugins/store";
import { DoorSaved } from "@/lib/model";
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
import { toast } from "bulma-toast";

View file

@ -14,8 +14,8 @@
</template>
<script lang="ts">
import { Door } from "@/lib/door";
import { advent22Store } from "@/plugins/store";
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
import SVGRect from "./SVGRect.vue";

View file

@ -18,8 +18,8 @@
</template>
<script lang="ts">
import { Rectangle } from "@/lib/rectangle";
import { advent22Store } from "@/plugins/store";
import { Rectangle } from "@/lib/rects/rectangle";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
type BulmaVariant =

View file

@ -16,8 +16,8 @@
</template>
<script lang="ts">
import { Vector2D } from "@/lib/vector2d";
import { advent22Store } from "@/plugins/store";
import { Vector2D } from "@/lib/rects/vector2d";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
function get_event_thous(event: MouseEvent): Vector2D {

View file

@ -24,9 +24,9 @@
</template>
<script lang="ts">
import { Door } from "@/lib/door";
import { Rectangle } from "@/lib/rectangle";
import { Vector2D } from "@/lib/vector2d";
import { Door } from "@/lib/rects/door";
import { Rectangle } from "@/lib/rects/rectangle";
import { Vector2D } from "@/lib/rects/vector2d";
import { Options, Vue } from "vue-class-component";
import CalendarDoor from "../calendar/CalendarDoor.vue";

View file

@ -24,8 +24,8 @@
</template>
<script lang="ts">
import { Door } from "@/lib/door";
import { advent22Store } from "@/plugins/store";
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
import ThouCanvas from "../calendar/ThouCanvas.vue";

View file

@ -16,8 +16,8 @@
</template>
<script lang="ts">
import { Door } from "@/lib/door";
import { advent22Store } from "@/plugins/store";
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import { Options, Vue } from "vue-class-component";
import DoorCanvas from "./DoorCanvas.vue";

View file

@ -23,7 +23,7 @@
</template>
<script lang="ts">
import { Door } from "@/lib/door";
import { Door } from "@/lib/rects/door";
import { Options, Vue } from "vue-class-component";
import SVGRect from "../calendar/SVGRect.vue";

View file

@ -1,10 +0,0 @@
import { Advent22 } from "@/plugins/advent22";
declare module "@vue/runtime-core" {
// bind to `this` keyword
interface ComponentCustomProperties {
$advent22: Advent22;
}
}
export {};

View file

@ -1,71 +1,82 @@
export interface AdminConfigModel {
solution: {
value: string;
whitespace: string;
special_chars: string;
case: string;
clean: string;
};
puzzle: {
first: string;
next: string | null;
last: string;
end: string;
seed: string;
extra_days: number[];
skip_empty: boolean;
};
calendar: {
config_file: string;
background: string;
favicon: string;
};
image: {
size: number;
border: number;
};
fonts: { file: string; size: number }[];
redis: {
host: string;
port: number;
db: number;
protocol: number;
};
webdav: {
url: string;
cache_ttl: number;
config_file: string;
};
import axios, {
AxiosBasicCredentials,
type AxiosRequestConfig,
type Method,
type RawAxiosRequestHeaders,
} from "axios";
import { APIError } from "./api_error";
interface Params {
endpoint: string;
method?: Method;
data?: unknown;
headers?: RawAxiosRequestHeaders;
config?: AxiosRequestConfig;
}
export interface SiteConfigModel {
title: string;
subtitle: string;
content: string;
footer: string;
}
export class API {
private static get api_baseurl(): string {
// in production mode, return "proto://hostname/api"
if (process.env.NODE_ENV === "production") {
return `${window.location.protocol}//${window.location.host}/api`;
} else if (process.env.NODE_ENV !== "development") {
// not in prouction or development mode
console.warn("Unexpected NODE_ENV value");
}
export interface NumStrDict {
[key: number]: string;
}
// in development mode, return "proto://hostname:8000/api"
return `${window.location.protocol}//${window.location.hostname}:8000/api`;
}
export interface DoorSaved {
day: number;
x1: number;
y1: number;
x2: number;
y2: number;
}
private static readonly axios = axios.create({
timeout: 10e3,
baseURL: this.api_baseurl,
});
export type Credentials = [username: string, password: string];
private static readonly storage_key = "advent22/credentials";
export function objForEach<T>(
obj: T,
f: (k: keyof T, v: T[keyof T]) => void,
): void {
for (const k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
f(k, obj[k]);
public static set creds(value: AxiosBasicCredentials | null) {
if (value === null) {
localStorage.removeItem(this.storage_key);
return;
}
localStorage.setItem(this.storage_key, JSON.stringify(value));
}
public static get creds(): AxiosBasicCredentials | undefined {
const auth_json = localStorage.getItem(this.storage_key);
if (auth_json !== null) return JSON.parse(auth_json);
}
private static get_axios_config({
endpoint,
method = "GET",
data,
headers = {},
config = {},
}: Params): AxiosRequestConfig {
return {
url: endpoint,
method: method,
data: data,
auth: this.creds,
headers: headers,
...config,
};
}
public static async request<T = string>(p: Params): Promise<T>;
public static async request<T = string>(p: string): Promise<T>;
public static async request<T = string>(p: Params | string): Promise<T> {
if (typeof p === "string") p = { endpoint: p };
try {
const response = await this.axios.request<T>(this.get_axios_config(p));
return response.data;
} catch (reason) {
console.error(`Failed to query ${p.endpoint}: ${reason}`);
throw new APIError(reason, p.endpoint);
}
}
}

73
ui/src/lib/api_error.ts Normal file
View file

@ -0,0 +1,73 @@
import { AxiosError } from "axios";
import { toast } from "bulma-toast";
export class APIError extends Error {
reason: unknown;
axios_error: AxiosError | null = null;
constructor(reason: unknown, endpoint: string) {
super(endpoint); // sets this.message to the endpoint
this.reason = reason;
Object.setPrototypeOf(this, APIError.prototype);
if (reason instanceof AxiosError) {
this.axios_error = reason;
}
}
public format(): string {
let msg =
"Unbekannter Fehler, bitte wiederholen! Besteht das Problem länger, bitte Admin benachrichtigen!";
let code = "U";
const result = () => `${msg} (Fehlercode: ${code}/${this.message})`;
if (this.axios_error === null) return result();
switch (this.axios_error.code) {
case "ECONNABORTED":
// API unerreichbar
msg =
"API antwortet nicht, bitte später wiederholen! Besteht das Problem länger, bitte Admin benachrichtigen!";
code = "D";
break;
case "ERR_NETWORK":
// Netzwerk nicht verbunden
msg = "Sieht aus, als sei deine Netzwerkverbindung gestört.";
code = "N";
break;
default:
if (this.axios_error.response === undefined) return result();
switch (this.axios_error.response.status) {
case 401:
// UNAUTHORIZED
msg = "Netter Versuch :)";
code = "A";
break;
case 422:
// UNPROCESSABLE ENTITY
msg = "Funktion ist kaputt, bitte Admin benachrichtigen!";
code = "I";
break;
default:
// HTTP
code = `H${this.axios_error.response.status}`;
break;
}
break;
}
return result();
}
public alert() {
toast({
message: this.format(),
type: "is-danger",
});
}
}

10
ui/src/lib/helpers.ts Normal file
View file

@ -0,0 +1,10 @@
export function objForEach<T>(
obj: T,
f: (k: keyof T, v: T[keyof T]) => void,
): void {
for (const k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) {
f(k, obj[k]);
}
}
}

67
ui/src/lib/model.ts Normal file
View file

@ -0,0 +1,67 @@
export interface AdminConfigModel {
solution: {
value: string;
whitespace: string;
special_chars: string;
case: string;
clean: string;
};
puzzle: {
first: string;
next: string | null;
last: string;
end: string;
seed: string;
extra_days: number[];
skip_empty: boolean;
};
calendar: {
config_file: string;
background: string;
favicon: string;
};
image: {
size: number;
border: number;
};
fonts: { file: string; size: number }[];
redis: {
host: string;
port: number;
db: number;
protocol: number;
};
webdav: {
url: string;
cache_ttl: number;
config_file: string;
};
}
export interface SiteConfigModel {
title: string;
subtitle: string;
content: string;
footer: string;
}
export interface NumStrDict {
[key: number]: string;
}
export interface DoorSaved {
day: number;
x1: number;
y1: number;
x2: number;
y2: number;
}
export interface ImageData {
height: number;
width: number;
aspect_ratio: number;
data_url: string;
}
export type Credentials = [username: string, password: string];

View file

@ -1,4 +1,4 @@
import { DoorSaved } from "./api";
import { DoorSaved } from "../model";
import { Rectangle } from "./rectangle";
import { Vector2D } from "./vector2d";

View file

@ -1,10 +1,7 @@
import { Credentials, DoorSaved, SiteConfigModel } from "@/lib/api";
import { Door } from "@/lib/door";
import { Advent22 } from "@/plugins/advent22";
import { RemovableRef, useLocalStorage } from "@vueuse/core";
import { AxiosBasicCredentials, AxiosError } from "axios";
import { toast } from "bulma-toast";
import { acceptHMRUpdate, defineStore } from "pinia";
import { API } from "./api";
import { Credentials, DoorSaved, SiteConfigModel } from "./model";
import { Door } from "./rects/door";
declare global {
interface Navigator {
@ -13,9 +10,6 @@ declare global {
}
type State = {
advent22: Advent22;
api_creds: RemovableRef<Credentials>;
is_initialized: boolean;
on_initialized: (() => void)[];
is_touch_device: boolean;
is_admin: boolean;
@ -30,9 +24,6 @@ export const advent22Store = defineStore({
id: "advent22",
state: (): State => ({
advent22: new Advent22(),
api_creds: useLocalStorage("advent22/auth", ["", ""]),
is_initialized: false,
on_initialized: [],
is_touch_device:
window.matchMedia("(any-hover: none)").matches ||
@ -52,81 +43,14 @@ export const advent22Store = defineStore({
next_door_target: null,
}),
getters: {
axios_creds: (state): AxiosBasicCredentials => {
const [username, password] = state.api_creds;
return { username: username, password: password };
},
},
actions: {
init(): void {
this.update()
.then(() => {
this.is_initialized = true;
for (const callback of this.on_initialized) callback();
})
.catch(this.alert_user_error);
},
format_user_error([reason, endpoint]: [unknown, string]): string {
let msg =
"Unbekannter Fehler, bitte wiederholen! Besteht das Problem länger, bitte Admin benachrichtigen!";
let code = "U";
const result = () => `${msg} (Fehlercode: ${code}/${endpoint})`;
if (!(reason instanceof AxiosError)) return result();
switch (reason.code) {
case "ECONNABORTED":
// API unerreichbar
msg =
"API antwortet nicht, bitte später wiederholen! Besteht das Problem länger, bitte Admin benachrichtigen!";
code = "D";
break;
case "ERR_NETWORK":
// Netzwerk nicht verbunden
msg = "Sieht aus, als sei deine Netzwerkverbindung gestört.";
code = "N";
break;
default:
if (reason.response === undefined) return result();
switch (reason.response.status) {
case 401:
// UNAUTHORIZED
msg = "Netter Versuch :)";
code = "A";
break;
case 422:
// UNPROCESSABLE ENTITY
msg = "Funktion ist kaputt, bitte Admin benachrichtigen!";
code = "I";
break;
default:
// HTTP
code = `H${reason.response.status}`;
break;
}
break;
}
return result();
},
alert_user_error(param: [unknown, string]): void {
toast({
message: this.format_user_error(param),
type: "is-danger",
});
this.update().then(() => this.on_initialized.forEach((fn) => fn()));
},
update(): Promise<void> {
return new Promise((resolve, reject) => {
API.request("user/favicon");
this.advent22
.api_get_blob("user/favicon")
.then((favicon_src) => {
@ -204,7 +128,7 @@ export const advent22Store = defineStore({
},
login(creds: Credentials): Promise<boolean> {
this.api_creds = creds;
API.creds = { username: creds[0], password: creds[1] };
return this.update_is_admin();
},

View file

@ -1,6 +1,6 @@
import { advent22Store } from "@/lib/store";
import { Advent22Plugin } from "@/plugins/advent22";
import { FontAwesomePlugin } from "@/plugins/fontawesome";
import { advent22Store } from "@/plugins/store";
import * as bulmaToast from "bulma-toast";
import { createPinia } from "pinia";
import { createApp } from "vue";

View file

@ -1,110 +0,0 @@
import axios, { AxiosInstance, ResponseType } from "axios";
import { App, Plugin } from "vue";
import { advent22Store } from "./store";
export class Advent22 {
private axios: AxiosInstance;
public constructor() {
this.axios = axios.create({
timeout: 10e3,
});
}
private get api_baseurl(): string {
// in production mode, return "//host/api"
if (process.env.NODE_ENV === "production") {
return `//${window.location.host}/api`;
} else if (process.env.NODE_ENV !== "development") {
// not in prouction or development mode
console.warn("Unexpected NODE_ENV value");
}
// in development mode, return "//hostname:8000/api"
return `//${window.location.hostname}:8000/api`;
}
public name_door(day: number): string {
return `Türchen ${day}`;
}
public api_url(): string;
public api_url(endpoint: string): string;
public api_url(endpoint?: string): string {
if (endpoint === undefined) {
return this.api_baseurl;
}
while (endpoint.startsWith("/")) {
endpoint = endpoint.substring(1);
}
return `${this.api_baseurl}/${endpoint}`;
}
private _api_get<T>(endpoint: string): Promise<T>;
private _api_get<T>(endpoint: string, responseType: ResponseType): Promise<T>;
private _api_get<T>(
endpoint: string,
responseType: ResponseType = "json",
): Promise<T> {
const req_config = {
auth: advent22Store().axios_creds,
responseType: responseType,
};
return new Promise<T>((resolve, reject) => {
this.axios
.get<T>(this.api_url(endpoint), req_config)
.then((response) => resolve(response.data))
.catch((reason) => {
console.error(`Failed to query ${endpoint}: ${reason}`);
reject([reason, endpoint]);
});
});
}
public api_get<T>(endpoint: string): Promise<T> {
return this._api_get<T>(endpoint);
}
public api_get_blob(endpoint: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
this._api_get<Blob>(endpoint, "blob")
.then((data: Blob) => {
const reader = new FileReader();
reader.readAsDataURL(data);
reader.onloadend = () => {
if (typeof reader.result === "string") {
resolve(reader.result);
} else {
reject(["failed data url", endpoint]);
}
};
})
.catch(reject);
});
}
public api_put(endpoint: string, data: unknown): Promise<void> {
const req_config = {
auth: advent22Store().axios_creds,
};
return new Promise<void>((resolve, reject) => {
this.axios
.put(this.api_url(endpoint), data, req_config)
.then(() => resolve())
.catch((reason) => {
console.error(`Failed to query ${endpoint}: ${reason}`);
reject([reason, endpoint]);
});
});
}
}
export const Advent22Plugin: Plugin = {
install(app: App) {
app.config.globalProperties.$advent22 = new Advent22();
},
};

View file

@ -1,7 +1,7 @@
import { expect } from "chai";
import { Rectangle } from "@/lib/rectangle";
import { Vector2D } from "@/lib/vector2d";
import { Rectangle } from "@/lib/rects/rectangle";
import { Vector2D } from "@/lib/rects/vector2d";
describe("Rectangle Tests", () => {
const v1 = new Vector2D(1, 2);

View file

@ -1,6 +1,6 @@
import { expect } from "chai";
import { Vector2D } from "@/lib/vector2d";
import { Vector2D } from "@/lib/rects/vector2d";
describe("Vector2D Tests", () => {
const v = new Vector2D(1, 2);