Compare commits

..

No commits in common. "3d42713e69b8979f0d02f28ab2cb17f20432341b" and "7643f35ec5e2101090a42ddf6dd73d76eec43627" have entirely different histories.

17 changed files with 119 additions and 98 deletions

View file

@ -3,7 +3,7 @@
<BulmaButton <BulmaButton
v-bind="$attrs" v-bind="$attrs"
:icon="['fas', store.is_admin ? 'fa-toggle-on' : 'fa-toggle-off']" :icon="'fa-solid fa-toggle-' + (store.is_admin ? 'on' : 'off')"
:busy="is_busy" :busy="is_busy"
text="Admin" text="Admin"
@click.left="on_click" @click.left="on_click"
@ -23,7 +23,7 @@ const modal_visible = ref(false);
const is_busy = ref(false); const is_busy = ref(false);
const store = advent22Store(); const store = advent22Store();
function on_click(): void { function on_click() {
if (store.is_admin) { if (store.is_admin) {
store.logout(); store.logout();
} else { } else {
@ -33,19 +33,16 @@ function on_click(): void {
} }
} }
async function on_submit(creds: Credentials): Promise<void> { function on_submit(creds: Credentials) {
modal_visible.value = false; modal_visible.value = false;
try { store
await store.login(creds); .login(creds)
} catch (error) { .catch((error) => APIError.alert(error))
APIError.alert(error); .finally(() => (is_busy.value = false));
} finally {
is_busy.value = false;
}
} }
function on_cancel(): void { function on_cancel() {
modal_visible.value = false; modal_visible.value = false;
is_busy.value = false; is_busy.value = false;
} }

View file

@ -71,11 +71,11 @@ let modal: HMultiModal | undefined;
let toast: HBulmaToast | undefined; let toast: HBulmaToast | undefined;
let toast_timeout: number | undefined; let toast_timeout: number | undefined;
function on_modal_handle(handle: HMultiModal): void { function on_modal_handle(handle: HMultiModal) {
modal = handle; modal = handle;
} }
function on_toast_handle(handle: HBulmaToast): void { function on_toast_handle(handle: HBulmaToast) {
toast = handle; toast = handle;
if (store.is_touch_device) return; if (store.is_touch_device) return;
@ -90,7 +90,7 @@ function on_toast_handle(handle: HBulmaToast): void {
}); });
} }
async function door_click(day: number): Promise<void> { async function door_click(day: number) {
window.clearTimeout(toast_timeout); window.clearTimeout(toast_timeout);
toast?.hide(); toast?.hide();

View file

@ -16,7 +16,7 @@
ref="username_input" ref="username_input"
class="input" class="input"
type="text" type="text"
v-model="creds[0]" v-model="username"
/> />
</div> </div>
</div> </div>
@ -24,7 +24,7 @@
<div class="field"> <div class="field">
<label class="label">Passwort</label> <label class="label">Passwort</label>
<div class="control"> <div class="control">
<input class="input" type="password" v-model="creds[1]" /> <input class="input" type="password" v-model="password" />
</div> </div>
</div> </div>
</section> </section>
@ -33,13 +33,13 @@
<BulmaButton <BulmaButton
class="is-success" class="is-success"
@click.left="submit" @click.left="submit"
:icon="['fas', 'fa-unlock']" icon="fa-solid fa-unlock"
text="Login" text="Login"
/> />
<BulmaButton <BulmaButton
class="is-danger" class="is-danger"
@click.left="cancel" @click.left="cancel"
:icon="['fas', 'fa-circle-xmark']" icon="fa-solid fa-circle-xmark"
text="Abbrechen" text="Abbrechen"
/> />
</footer> </footer>
@ -60,10 +60,11 @@ const emit = defineEmits<{
(event: "cancel"): void; (event: "cancel"): void;
}>(); }>();
const creds = ref<Credentials>(["", ""]); const username = ref("");
const password = ref("");
function submit(): void { function submit(): void {
emit("submit", creds.value); emit("submit", [username.value, password.value]);
} }
function cancel(): void { function cancel(): void {

View file

@ -39,17 +39,18 @@ export type HMultiModal = {
show_image(src: string, caption: string): void; show_image(src: string, caption: string): void;
show_loading(): void; show_loading(): void;
hide(): void; hide(): void;
dismiss(): void;
}; };
const emit = defineEmits<{ const emit = defineEmits<{
(event: "handle", handle: HMultiModal): void; (event: "handle", handle: HMultiModal): void;
}>(); }>();
function hide(): void { function hide() {
state.value = { show: "none" }; state.value = { show: "none" };
} }
function dismiss(): void { function dismiss() {
if (state.value.show !== "loading") { if (state.value.show !== "loading") {
hide(); hide();
} }
@ -57,13 +58,14 @@ function dismiss(): void {
onMounted(() => { onMounted(() => {
emit("handle", { emit("handle", {
show_image(src: string, caption: string = ""): void { show_image(src: string, caption: string = "") {
state.value = { show: "image", src: src, caption: caption }; state.value = { show: "image", src: src, caption: caption };
}, },
show_loading(): void { show_loading() {
state.value = { show: "loading" }; state.value = { show: "loading" };
}, },
hide, hide,
dismiss,
}); });
const on_keydown = (e: KeyboardEvent) => { const on_keydown = (e: KeyboardEvent) => {

View file

@ -2,7 +2,10 @@
<span>Eingabemodus:&nbsp;</span> <span>Eingabemodus:&nbsp;</span>
<BulmaButton <BulmaButton
v-bind="$attrs" v-bind="$attrs"
:icon="['fas', store.is_touch_device ? 'hand-pointer' : 'arrow-pointer']" :icon="
'fa-solid fa-' +
(store.is_touch_device ? 'hand-pointer' : 'arrow-pointer')
"
:text="store.is_touch_device ? 'Touch' : 'Desktop'" :text="store.is_touch_device ? 'Touch' : 'Desktop'"
@click.left="store.toggle_touch_device" @click.left="store.toggle_touch_device"
/> />

View file

@ -35,7 +35,7 @@
v-for="(data, day) in day_data" v-for="(data, day) in day_data"
:key="`btn-${day}`" :key="`btn-${day}`"
:class="'tag is-' + (data.part === '' ? 'warning' : 'info')" :class="'tag is-' + (data.part === '' ? 'warning' : 'info')"
:icon="['fas', 'fa-door-open']" icon="fa-solid fa-door-open"
:text="day.toString()" :text="day.toString()"
@click.left="door_click(Number(day))" @click.left="door_click(Number(day))"
/> />
@ -59,11 +59,11 @@ const day_data = ref<Record<number, { part: string; image_name: string }>>({});
let modal: HMultiModal | undefined; let modal: HMultiModal | undefined;
function on_modal_handle(handle: HMultiModal): void { function on_modal_handle(handle: HMultiModal) {
modal = handle; modal = handle;
} }
async function on_open(): Promise<void> { async function on_open() {
const [day_parts, day_image_names] = await Promise.all([ const [day_parts, day_image_names] = await Promise.all([
API.request<NumStrDict>("admin/day_parts"), API.request<NumStrDict>("admin/day_parts"),
API.request<NumStrDict>("admin/day_image_names"), API.request<NumStrDict>("admin/day_image_names"),
@ -86,7 +86,7 @@ async function on_open(): Promise<void> {
}); });
} }
async function door_click(day: number): Promise<void> { async function door_click(day: number) {
if (modal === undefined) return; if (modal === undefined) return;
modal.show_loading(); modal.show_loading();

View file

@ -247,19 +247,19 @@ function fmt_puzzle_date(name: keyof AdminConfigModel["puzzle"]): string {
return DateTime.fromISO(iso_date).toLocaleString(DateTime.DATE_SHORT); return DateTime.fromISO(iso_date).toLocaleString(DateTime.DATE_SHORT);
} }
async function on_open(): Promise<void> { async function on_open() {
const [store_update, new_admin_config_model, new_doors] = await Promise.all([ const [store_update, new_admin_config_model, new_doors] = await Promise.all([
store.update(), store.update(),
API.request<AdminConfigModel>("admin/config_model"), API.request<AdminConfigModel>("admin/config_model"),
API.request<DoorSaved[]>("admin/doors"), API.request<DoorSaved[]>("admin/doors"),
]); ]);
void store_update; // discard value void store_update;
admin_config_model.value = new_admin_config_model; admin_config_model.value = new_admin_config_model;
doors.value = new_doors; doors.value = new_doors;
} }
async function load_dav_credentials(): Promise<void> { async function load_dav_credentials() {
try { try {
dav_credentials.value = await API.request<Credentials>( dav_credentials.value = await API.request<Credentials>(
"admin/dav_credentials", "admin/dav_credentials",
@ -267,7 +267,7 @@ async function load_dav_credentials(): Promise<void> {
} catch {} } catch {}
} }
async function load_ui_credentials(): Promise<void> { async function load_ui_credentials() {
try { try {
ui_credentials.value = await API.request<Credentials>( ui_credentials.value = await API.request<Credentials>(
"admin/ui_credentials", "admin/ui_credentials",

View file

@ -5,7 +5,7 @@
:disabled="current_step === 0" :disabled="current_step === 0"
class="level-item is-link" class="level-item is-link"
@click="current_step--" @click="current_step--"
:icon="['fas', 'fa-backward']" icon="fa-solid fa-backward"
/> />
<BulmaBreadcrumbs <BulmaBreadcrumbs
@ -18,7 +18,7 @@
:disabled="current_step === 2" :disabled="current_step === 2"
class="level-item is-link" class="level-item is-link"
@click="current_step++" @click="current_step++"
:icon="['fas', 'fa-forward']" icon="fa-solid fa-forward"
/> />
</nav> </nav>
@ -47,20 +47,20 @@
<BulmaButton <BulmaButton
class="card-footer-item is-danger" class="card-footer-item is-danger"
@click="on_download" @click="on_download"
:icon="['fas', 'fa-cloud-arrow-down']" icon="fa-solid fa-cloud-arrow-down"
:busy="loading_doors" :busy="loading_doors"
text="Laden" text="Laden"
/> />
<BulmaButton <BulmaButton
class="card-footer-item is-warning" class="card-footer-item is-warning"
@click="on_discard" @click="on_discard"
:icon="['fas', 'fa-trash']" icon="fa-solid fa-trash"
text="Löschen" text="Löschen"
/> />
<BulmaButton <BulmaButton
class="card-footer-item is-success" class="card-footer-item is-success"
@click="on_upload" @click="on_upload"
:icon="['fas', 'fa-cloud-arrow-up']" icon="fa-solid fa-cloud-arrow-up"
:busy="saving_doors" :busy="saving_doors"
text="Speichern" text="Speichern"
/> />
@ -85,9 +85,9 @@ import DoorChooser from "../editor/DoorChooser.vue";
import DoorPlacer from "../editor/DoorPlacer.vue"; import DoorPlacer from "../editor/DoorPlacer.vue";
const steps: BCStep[] = [ const steps: BCStep[] = [
{ label: "Platzieren", icon: ["fas", "fa-crosshairs"] }, { label: "Platzieren", icon: "fa-solid fa-crosshairs" },
{ label: "Ordnen", icon: ["fas", "fa-list-ol"] }, { label: "Ordnen", icon: "fa-solid fa-list-ol" },
{ label: "Vorschau", icon: ["fas", "fa-magnifying-glass"] }, { label: "Vorschau", icon: "fa-solid fa-magnifying-glass" },
]; ];
const doors = ref<Door[]>([]); const doors = ref<Door[]>([]);
@ -95,7 +95,7 @@ const current_step = ref(0);
const loading_doors = ref(false); const loading_doors = ref(false);
const saving_doors = ref(false); const saving_doors = ref(false);
async function load_doors(): Promise<void> { async function load_doors() {
try { try {
const data = await API.request<DoorSaved[]>("admin/doors"); const data = await API.request<DoorSaved[]>("admin/doors");
@ -109,7 +109,7 @@ async function load_doors(): Promise<void> {
} }
} }
async function save_doors(): Promise<void> { async function save_doors() {
try { try {
const data: DoorSaved[] = []; const data: DoorSaved[] = [];
@ -128,7 +128,7 @@ async function save_doors(): Promise<void> {
} }
} }
async function on_download(): Promise<void> { async function on_download() {
if (confirm("Aktuelle Änderungen verwerfen und Status vom Server laden?")) { if (confirm("Aktuelle Änderungen verwerfen und Status vom Server laden?")) {
loading_doors.value = true; loading_doors.value = true;
@ -146,14 +146,14 @@ async function on_download(): Promise<void> {
} }
} }
function on_discard(): void { function on_discard() {
if (confirm("Alle Türchen löschen? (nur lokal)")) { if (confirm("Alle Türchen löschen? (nur lokal)")) {
// empty `doors` array // empty `doors` array
doors.value.length = 0; doors.value.length = 0;
} }
} }
async function on_upload(): Promise<void> { async function on_upload() {
if (confirm("Aktuelle Änderungen an den Server schicken?")) { if (confirm("Aktuelle Änderungen an den Server schicken?")) {
saving_doors.value = true; saving_doors.value = true;

View file

@ -22,21 +22,20 @@
</button> </button>
</header> </header>
<slot v-if="state === 'loading'" name="loading"> <template v-if="is_open">
<div class="card-content"> <div v-if="state === 'loading'" class="card-content">
<progress class="progress is-primary" /> <progress class="progress is-primary" />
</div> </div>
</slot> <div
v-else-if="state === 'err'"
<slot v-else-if="state === 'err'" name="error"> class="card-content has-text-danger has-text-centered"
<div class="card-content has-text-danger has-text-centered"> >
<span class="icon is-large"> <span class="icon is-large">
<FontAwesomeIcon :icon="['fas', 'ban']" size="3x" /> <FontAwesomeIcon :icon="['fas', 'ban']" size="3x" />
</span> </span>
</div> </div>
</slot> <slot v-else name="default" />
</template>
<slot v-else-if="state === 'ok'" name="default" />
</div> </div>
</template> </template>
@ -57,7 +56,7 @@ const props = withDefaults(
const state = ref<"closed" | "loading" | "ok" | "err">("closed"); const state = ref<"closed" | "loading" | "ok" | "err">("closed");
const is_open = computed(() => state.value !== "closed"); const is_open = computed(() => state.value !== "closed");
async function toggle(): Promise<void> { async function toggle() {
if (is_open.value) { if (is_open.value) {
state.value = "closed"; state.value = "closed";
} else { } else {
@ -65,7 +64,7 @@ async function toggle(): Promise<void> {
} }
} }
async function load(): Promise<void> { async function load() {
state.value = "loading"; state.value = "loading";
try { try {

View file

@ -20,27 +20,29 @@ const emit = defineEmits<{
(event: "handle", handle: HBulmaToast): void; (event: "handle", handle: HBulmaToast): void;
}>(); }>();
const message_div = useTemplateRef("message"); const message = useTemplateRef("message");
onMounted(() => onMounted(() =>
emit("handle", { emit("handle", {
show(options: ToastOptions = {}): void { show(options: ToastOptions = {}) {
if (message_div.value === null) return; if (message.value === null) return;
toast({ toast({
...options, ...options,
single: true, single: true,
message: message_div.value, message: message.value,
}); });
}, },
hide(): void { hide() {
// using "toast" detaches "message" from the invisible "div" if (message.value === null) return;
// => toast_div is not part of this component!
const toast_div = message_div.value?.parentElement;
const delete_button = toast_div?.querySelector("button.delete");
if (!(delete_button instanceof HTMLButtonElement)) return;
delete_button.click(); const toast_div = message.value.parentElement;
if (toast_div === null) return;
const dbutton = toast_div.querySelector("button.delete");
if (!(dbutton instanceof HTMLButtonElement)) return;
dbutton.click();
}, },
}), }),
); );

View file

@ -42,7 +42,7 @@ const emit = defineEmits<{
(event: TCEventType, e: MouseEvent, point: Vector2D): void; (event: TCEventType, e: MouseEvent, point: Vector2D): void;
}>(); }>();
function transform_mouse_event(event: MouseEvent): void { function transform_mouse_event(event: MouseEvent) {
if (!is_tceventtype(event.type)) return; if (!is_tceventtype(event.type)) return;
emit(event.type, event, get_event_thous(event)); emit(event.type, event, get_event_thous(event));

View file

@ -50,20 +50,26 @@ const preview_visible = computed(() => state.value.kind !== "idle");
function pop_door(point: Vector2D): VueLike<Door> | undefined { function pop_door(point: Vector2D): VueLike<Door> | undefined {
const idx = model.value.findIndex((rect) => rect.position.contains(point)); const idx = model.value.findIndex((rect) => rect.position.contains(point));
if (idx === -1) return; if (idx === -1) {
return;
}
return model.value.splice(idx, 1)[0]; return model.value.splice(idx, 1)[0];
} }
function draw_start(event: MouseEvent, point: Vector2D): void { function draw_start(event: MouseEvent, point: Vector2D) {
if (preview_visible.value) return; if (preview_visible.value) {
return;
}
preview.value = new Rectangle(point, point); preview.value = new Rectangle(point, point);
state.value = { kind: "drawing" }; state.value = { kind: "drawing" };
} }
function draw_finish(): void { function draw_finish() {
if (state.value.kind !== "drawing") return; if (state.value.kind !== "drawing") {
return;
}
if (preview.value.area >= MIN_RECT_AREA) { if (preview.value.area >= MIN_RECT_AREA) {
model.value.push(new Door(preview.value)); model.value.push(new Door(preview.value));
@ -72,27 +78,33 @@ function draw_finish(): void {
state.value = { kind: "idle" }; state.value = { kind: "idle" };
} }
function drag_start(event: MouseEvent, point: Vector2D): void { function drag_start(event: MouseEvent, point: Vector2D) {
if (preview_visible.value) return; if (preview_visible.value) {
return;
}
const drag_door = pop_door(point); const drag_door = pop_door(point);
if (drag_door === undefined) return; if (drag_door === undefined) {
return;
}
preview.value = drag_door.position; preview.value = drag_door.position;
state.value = { kind: "dragging", door: drag_door, origin: point }; state.value = { kind: "dragging", door: drag_door, origin: point };
} }
function drag_finish(): void { function drag_finish() {
if (state.value.kind !== "dragging") return; if (state.value.kind !== "dragging") {
return;
}
model.value.push(new Door(preview.value, state.value.door.day)); model.value.push(new Door(preview.value, state.value.door.day));
state.value = { kind: "idle" }; state.value = { kind: "idle" };
} }
function on_mousemove(event: MouseEvent, point: Vector2D): void { function on_mousemove(event: MouseEvent, point: Vector2D) {
if (state.value.kind === "drawing") { if (state.value.kind === "drawing") {
preview.value = preview.value.update(undefined, point); preview.value = preview.value.update(undefined, point);
} else if (state.value.kind === "dragging") { } else if (state.value.kind === "dragging") {
@ -101,8 +113,10 @@ function on_mousemove(event: MouseEvent, point: Vector2D): void {
} }
} }
function remove_rect(event: MouseEvent, point: Vector2D): void { function remove_rect(event: MouseEvent, point: Vector2D) {
if (preview_visible.value) return; if (preview_visible.value) {
return;
}
pop_door(point); pop_door(point);
} }

View file

@ -35,13 +35,15 @@ const day_input = useTemplateRef("day_input");
const day_str = ref(""); const day_str = ref("");
const editing = ref(false); const editing = ref(false);
function toggle_editing(): void { function toggle_editing() {
day_str.value = String(model.value.day); day_str.value = String(model.value.day);
editing.value = !editing.value; editing.value = !editing.value;
} }
function on_click(event: MouseEvent): void { function on_click(event: MouseEvent) {
if (!(event.target instanceof HTMLDivElement)) return; if (!(event.target instanceof HTMLDivElement)) {
return;
}
if (editing.value) { if (editing.value) {
unwrap_vuelike(model.value).day = day_str.value; unwrap_vuelike(model.value).day = day_str.value;
@ -55,8 +57,10 @@ function on_click(event: MouseEvent): void {
toggle_editing(); toggle_editing();
} }
function on_keydown(event: KeyboardEvent): void { function on_keydown(event: KeyboardEvent) {
if (!editing.value) return; if (!editing.value) {
return;
}
if (event.key === "Enter") { if (event.key === "Enter") {
unwrap_vuelike(model.value).day = day_str.value; unwrap_vuelike(model.value).day = day_str.value;

View file

@ -62,14 +62,14 @@ export class APIError extends Error {
return result(); return result();
} }
public alert(): void { public alert() {
toast({ toast({
message: this.format(), message: this.format(),
type: "is-danger", type: "is-danger",
}); });
} }
public static alert(error: unknown): void { public static alert(error: unknown) {
new APIError(error, "").alert(); new APIError(error, "").alert();
} }
} }

View file

@ -26,7 +26,7 @@ export function unwrap_loading<T>(o: Loading<T>): T {
return o; return o;
} }
export function wait_for(condition: () => boolean, action: () => void): void { export function wait_for(condition: () => boolean, action: () => void) {
const enqueue_action = () => { const enqueue_action = () => {
if (!condition()) { if (!condition()) {
nextTick(enqueue_action); nextTick(enqueue_action);
@ -38,7 +38,7 @@ export function wait_for(condition: () => boolean, action: () => void): void {
enqueue_action(); enqueue_action();
} }
export function handle_error(error: unknown): void { export function handle_error(error: unknown) {
if (error instanceof APIError) { if (error instanceof APIError) {
error.alert(); error.alert();
} else { } else {

View file

@ -76,7 +76,7 @@ export const advent22Store = defineStore({
API.request<DoorSaved[]>("user/doors"), API.request<DoorSaved[]>("user/doors"),
API.request<number | null>("user/next_door"), API.request<number | null>("user/next_door"),
]); ]);
void is_admin; // discard value is_admin; // discard value
document.title = site_config.title; document.title = site_config.title;
@ -110,14 +110,13 @@ export const advent22Store = defineStore({
return this.is_admin; return this.is_admin;
}, },
async login(creds: Credentials): Promise<boolean> { login(creds: Credentials): Promise<boolean> {
API.creds = { username: creds[0], password: creds[1] }; API.creds = { username: creds[0], password: creds[1] };
return await this.update_is_admin(); return this.update_is_admin();
}, },
logout() { logout(): Promise<boolean> {
API.creds = { username: "", password: "" }; return this.login(["", ""]);
this.is_admin = false;
}, },
toggle_touch_device(): void { toggle_touch_device(): void {

View file

@ -16,7 +16,7 @@ describe("Rectangle Tests", () => {
top: number, top: number,
width: number, width: number,
height: number, height: number,
): void { ) {
expect(r.left).to.equal(left); expect(r.left).to.equal(left);
expect(r.top).to.equal(top); expect(r.top).to.equal(top);