🚧 wip on ui: rework for vue 3 composition API

This commit is contained in:
Jörn-Michael Miehe 2025-12-07 03:28:00 +00:00
parent 9079fca30f
commit 39b56d8bd0
18 changed files with 118 additions and 115 deletions

View file

@ -33,10 +33,10 @@
</div>
<div class="level-right">
<div class="level-item">
<TouchButton class="tag is-warning" />
<TouchButton class="is-small is-warning" />
</div>
<div class="level-item">
<AdminButton class="tag is-link is-outlined" />
<AdminButton class="is-small is-link is-outlined" />
</div>
</div>
</div>

View file

@ -14,12 +14,13 @@
import { APIError } from "@/lib/api_error";
import { Credentials } from "@/lib/model";
import { advent22Store } from "@/lib/store";
import { ref } from "vue";
import BulmaButton from "./bulma/Button.vue";
import LoginModal from "./LoginModal.vue";
let modal_visible = false;
let is_busy = false;
const modal_visible = ref(false);
const is_busy = ref(false);
const store = advent22Store();
function on_click() {
@ -27,22 +28,22 @@ function on_click() {
store.logout();
} else {
// show login modal
is_busy = true;
modal_visible = true;
is_busy.value = true;
modal_visible.value = true;
}
}
function on_submit(creds: Credentials) {
modal_visible = false;
modal_visible.value = false;
store
.login(creds)
.catch((error) => APIError.alert(error))
.finally(() => (is_busy = false));
.finally(() => (is_busy.value = false));
}
function on_cancel() {
modal_visible = false;
is_busy = false;
modal_visible.value = false;
is_busy.value = false;
}
</script>

View file

@ -51,7 +51,7 @@ import { API } from "@/lib/api";
import { APIError } from "@/lib/api_error";
import { ensure_loaded, name_door } from "@/lib/helpers";
import { ImageData } from "@/lib/model";
import { Door } from "@/lib/rects/door";
import { VueDoor } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import { onBeforeUnmount } from "vue";
@ -62,7 +62,7 @@ import CalendarDoor from "./calendar/CalendarDoor.vue";
import ThouCanvas from "./calendar/ThouCanvas.vue";
defineProps<{
doors: Door[];
doors: VueDoor[];
}>();
const store = advent22Store();
@ -81,7 +81,7 @@ function on_toast_handle(handle: HBulmaToast) {
if (store.is_touch_device) return;
store.when_initialized(() => {
toast_timeout = setTimeout(() => {
toast_timeout = window.setTimeout(() => {
if (store.user_doors.length === 0) return;
if (store.is_touch_device) return;
@ -91,7 +91,7 @@ function on_toast_handle(handle: HBulmaToast) {
}
async function door_click(day: number) {
if (toast_timeout !== undefined) clearTimeout(toast_timeout);
window.clearTimeout(toast_timeout);
toast?.hide();
if (modal === undefined) return;

View file

@ -4,24 +4,25 @@
<script setup lang="ts">
import { Duration } from "luxon";
import { onBeforeUnmount, onMounted } from "vue";
import { onBeforeUnmount, onMounted, ref } from "vue";
const props = withDefaults(
defineProps<{
until: number;
tick_time: number;
tick_time?: number;
}>(),
{ tick_time: 200 },
);
let interval_id: number | null = null;
let string_repr = "";
let interval_id: number | undefined;
const string_repr = ref("");
function tick(): void {
onMounted(() => {
function tick(): void {
const distance_ms = props.until - Date.now();
if (distance_ms <= 0) {
string_repr = "Jetzt!";
string_repr.value = "Jetzt!";
return;
}
@ -30,20 +31,18 @@ function tick(): void {
const d_hms = distance.minus(d_days).shiftTo("hour", "minute", "second");
if (d_days.days > 0) {
string_repr = d_days.toHuman() + " ";
string_repr.value = d_days.toHuman() + " ";
} else {
string_repr = "";
string_repr.value = "";
}
string_repr.value += d_hms.toFormat("hh:mm:ss");
}
string_repr += d_hms.toFormat("hh:mm:ss");
}
onMounted(() => {
tick();
interval_id = window.setInterval(tick, props.tick_time);
});
onBeforeUnmount(() => {
if (interval_id === null) return;
window.clearInterval(interval_id);
});
</script>

View file

@ -52,7 +52,6 @@ import { Credentials } from "@/lib/model";
import { nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef } from "vue";
import BulmaButton from "./bulma/Button.vue";
defineProps<{ visible: boolean }>();
const username_input = useTemplateRef("username_input");
const emit = defineEmits<{

View file

@ -1,5 +1,5 @@
<template>
<Calendar :doors="doors" />
<Calendar :doors="store.user_doors" />
<hr />
<div class="content" v-html="store.site_config.content" />
<div class="content has-text-primary">
@ -20,12 +20,10 @@
</template>
<script setup lang="ts">
import { Door } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import Calendar from "./Calendar.vue";
import CountDown from "./CountDown.vue";
const store = advent22Store();
const doors = store.user_doors as Door[];
</script>

View file

@ -49,17 +49,18 @@
import { API } from "@/lib/api";
import { name_door, objForEach } from "@/lib/helpers";
import { ImageData, NumStrDict } from "@/lib/model";
import { ref } from "vue";
import MultiModal, { HMultiModal } from "../MultiModal.vue";
import BulmaButton from "../bulma/Button.vue";
import BulmaDrawer from "../bulma/Drawer.vue";
const day_data: {
const day_data = ref<{
[day: number]: {
part: string;
image_name: string;
};
} = {};
}>({});
let modal: HMultiModal | undefined;
@ -74,19 +75,19 @@ function on_open(ready: () => void, fail: () => void) {
])
.then(([day_parts, day_image_names]) => {
const _ensure_day_in_data = (day: number) => {
if (!(day in day_data)) {
day_data[day] = { part: "", image_name: "" };
if (!(day in day_data.value)) {
day_data.value[day] = { part: "", image_name: "" };
}
};
objForEach(day_parts, (day, part) => {
_ensure_day_in_data(day);
day_data[day].part = part;
day_data.value[day].part = part;
});
objForEach(day_image_names, (day, image_name) => {
_ensure_day_in_data(day);
day_data[day].image_name = image_name;
day_data.value[day].image_name = image_name;
});
ready();

View file

@ -188,6 +188,7 @@ import { API } from "@/lib/api";
import { AdminConfigModel, Credentials, DoorSaved } from "@/lib/model";
import { advent22Store } from "@/lib/store";
import { DateTime } from "luxon";
import { ref } from "vue";
import BulmaDrawer from "../bulma/Drawer.vue";
import BulmaSecret from "../bulma/Secret.vue";
@ -195,7 +196,7 @@ import CountDown from "../CountDown.vue";
const store = advent22Store();
let admin_config_model: AdminConfigModel = {
const admin_config_model = ref<AdminConfigModel>({
solution: {
value: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
whitespace: "KEEP",
@ -233,14 +234,14 @@ let admin_config_model: AdminConfigModel = {
cache_ttl: 0,
config_file: "sed diam nonumy",
},
};
});
let doors: DoorSaved[] = [];
let dav_credentials: Credentials = ["", ""];
let ui_credentials: Credentials = ["", ""];
const doors = ref<DoorSaved[]>([]);
const dav_credentials = ref<Credentials>(["", ""]);
const ui_credentials = ref<Credentials>(["", ""]);
function fmt_puzzle_date(name: keyof AdminConfigModel["puzzle"]): string {
const iso_date = admin_config_model.puzzle[name];
const iso_date = admin_config_model.value.puzzle[name];
if (!(typeof iso_date === "string")) return "-";
return DateTime.fromISO(iso_date).toLocaleString(DateTime.DATE_SHORT);
@ -255,8 +256,8 @@ function on_open(ready: () => void, fail: () => void): void {
.then(([store_update, new_admin_config_model, new_doors]) => {
store_update; // discard value
admin_config_model = new_admin_config_model;
doors = new_doors;
admin_config_model.value = new_admin_config_model;
doors.value = new_doors;
ready();
})
@ -265,13 +266,13 @@ function on_open(ready: () => void, fail: () => void): void {
function load_dav_credentials(): void {
API.request<Credentials>("admin/dav_credentials")
.then((creds) => (dav_credentials = creds))
.then((creds) => (dav_credentials.value = creds))
.catch(() => {});
}
function load_ui_credentials(): void {
API.request<Credentials>("admin/ui_credentials")
.then((creds) => (ui_credentials = creds))
.then((creds) => (ui_credentials.value = creds))
.catch(() => {});
}
</script>

View file

@ -37,10 +37,10 @@
</div>
</div>
<DoorPlacer v-if="current_step === 0" v-model="doors" />
<DoorChooser v-if="current_step === 1" v-model="doors" />
<DoorPlacer v-if="current_step === 0" v-model="(doors as Door[])" />
<DoorChooser v-if="current_step === 1" v-model="(doors as Door[])" />
<div v-if="current_step === 2" class="card-content">
<Calendar :doors="doors" />
<Calendar :doors="(doors as Door[])" />
</div>
<footer class="card-footer is-flex is-justify-content-space-around">
@ -71,11 +71,11 @@
<script setup lang="ts">
import { API } from "@/lib/api";
import { APIError } from "@/lib/api_error";
import { Step } from "@/lib/helpers";
import { DoorSaved } from "@/lib/model";
import { Door } from "@/lib/rects/door";
import { toast } from "bulma-toast";
import { ref } from "vue";
import { BCStep } from "../bulma/Breadcrumbs.vue";
import Calendar from "../Calendar.vue";
import BulmaBreadcrumbs from "../bulma/Breadcrumbs.vue";
@ -84,26 +84,25 @@ import BulmaDrawer from "../bulma/Drawer.vue";
import DoorChooser from "../editor/DoorChooser.vue";
import DoorPlacer from "../editor/DoorPlacer.vue";
const steps: Step[] = [
const steps: BCStep[] = [
{ label: "Platzieren", icon: "fa-solid fa-crosshairs" },
{ label: "Ordnen", icon: "fa-solid fa-list-ol" },
{ label: "Vorschau", icon: "fa-solid fa-magnifying-glass" },
];
const doors: Door[] = [];
const doors = ref<Door[]>([]);
const current_step = ref(0);
let loading_doors = false;
let saving_doors = false;
const loading_doors = ref(false);
const saving_doors = ref(false);
function load_doors(): Promise<void> {
return new Promise<void>((resolve, reject) => {
API.request<DoorSaved[]>("admin/doors")
.then((data) => {
doors.length = 0;
doors.value.length = 0;
for (const value of data) {
doors.push(Door.load(value));
doors.value.push(Door.load(value));
}
resolve();
@ -119,7 +118,7 @@ function save_doors(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const data: DoorSaved[] = [];
for (const door of doors) {
for (const door of doors.value) {
data.push(door.save());
}
@ -138,7 +137,7 @@ function on_open(ready: () => void, fail: () => void): void {
function on_download() {
if (confirm("Aktuelle Änderungen verwerfen und Status vom Server laden?")) {
loading_doors = true;
loading_doors.value = true;
load_doors()
.then(() =>
@ -149,20 +148,20 @@ function on_download() {
}),
)
.catch(() => {})
.finally(() => (loading_doors = false));
.finally(() => (loading_doors.value = false));
}
}
function on_discard() {
if (confirm("Alle Türchen löschen? (nur lokal)")) {
// empty `doors` array
doors.length = 0;
doors.value.length = 0;
}
}
function on_upload() {
if (confirm("Aktuelle Änderungen an den Server schicken?")) {
saving_doors = true;
saving_doors.value = true;
save_doors()
.then(() => {
@ -175,9 +174,9 @@ function on_upload() {
}),
)
.catch(() => {})
.finally(() => (saving_doors = false));
.finally(() => (saving_doors.value = false));
})
.catch(() => (saving_doors = false));
.catch(() => (saving_doors.value = false));
}
}
</script>

View file

@ -20,11 +20,14 @@
</template>
<script setup lang="ts">
import { Step } from "@/lib/helpers";
export interface BCStep {
label: string;
icon: string | string[];
}
const model = defineModel<number>({ required: true });
defineProps<{
steps: Step[];
steps: BCStep[];
}>();
</script>

View file

@ -9,7 +9,7 @@
<script setup lang="ts">
import { Options as ToastOptions, toast } from "bulma-toast";
import { onMounted, ref } from "vue";
import { onMounted, useTemplateRef } from "vue";
export type HBulmaToast = {
show(options: ToastOptions): void;
@ -20,7 +20,7 @@ const emit = defineEmits<{
(event: "handle", handle: HBulmaToast): void;
}>();
const message = ref<HTMLDivElement | null>(null);
const message = useTemplateRef("message");
onMounted(() =>
emit("handle", {
@ -34,10 +34,10 @@ onMounted(() =>
});
},
hide() {
if (!(message.value instanceof HTMLElement)) return;
if (message.value === null) return;
const toast_div = message.value.parentElement;
if (!(toast_div instanceof HTMLDivElement)) return;
if (toast_div === null) return;
const dbutton = toast_div.querySelector("button.delete");
if (!(dbutton instanceof HTMLButtonElement)) return;

View file

@ -14,7 +14,7 @@
</template>
<script setup lang="ts">
import { Door } from "@/lib/rects/door";
import { VueDoor } from "@/lib/rects/door";
import { advent22Store } from "@/lib/store";
import SVGRect from "./SVGRect.vue";
@ -23,8 +23,8 @@ const store = advent22Store();
withDefaults(
defineProps<{
door: Door;
force_visible: boolean;
door: VueDoor;
force_visible?: boolean;
}>(),
{
force_visible: false,

View file

@ -21,7 +21,7 @@
<script setup lang="ts">
import { loading_success } from "@/lib/helpers";
import { Rectangle } from "@/lib/rects/rectangle";
import { VueRectangle } from "@/lib/rects/rectangle";
import { advent22Store } from "@/lib/store";
const store = advent22Store();
@ -38,7 +38,7 @@ withDefaults(
defineProps<{
variant: BulmaVariant;
visible?: boolean;
rectangle: Rectangle;
rectangle: VueRectangle;
}>(),
{
visible: true,

View file

@ -27,6 +27,7 @@
import { Door } from "@/lib/rects/door";
import { Rectangle } from "@/lib/rects/rectangle";
import { Vector2D } from "@/lib/rects/vector2d";
import { ref } from "vue";
import CalendarDoor from "../calendar/CalendarDoor.vue";
import SVGRect from "../calendar/SVGRect.vue";
@ -40,11 +41,11 @@ type CanvasState =
const model = defineModel<Door[]>({ required: true });
const MIN_RECT_AREA = 300;
let state: CanvasState = { kind: "idle" };
let preview = new Rectangle();
const state = ref<CanvasState>({ kind: "idle" });
const preview = ref(new Rectangle());
function preview_visible(): boolean {
return state.kind !== "idle";
return state.value.kind !== "idle";
}
function pop_door(point: Vector2D): Door | undefined {
@ -62,18 +63,18 @@ function draw_start(event: MouseEvent, point: Vector2D) {
return;
}
preview = new Rectangle(point, point);
state = { kind: "drawing" };
preview.value = new Rectangle(point, point);
state.value = { kind: "drawing" };
}
function draw_finish() {
if (state.kind !== "drawing" || preview.area < MIN_RECT_AREA) {
if (state.value.kind !== "drawing" || preview.value.area < MIN_RECT_AREA) {
return;
}
model.value.push(new Door(preview));
model.value.push(new Door(preview.value as Rectangle));
state = { kind: "idle" };
state.value = { kind: "idle" };
}
function drag_start(event: MouseEvent, point: Vector2D) {
@ -87,27 +88,27 @@ function drag_start(event: MouseEvent, point: Vector2D) {
return;
}
preview = drag_door.position;
preview.value = drag_door.position;
state = { kind: "dragging", door: drag_door, origin: point };
state.value = { kind: "dragging", door: drag_door, origin: point };
}
function drag_finish() {
if (state.kind !== "dragging") {
if (state.value.kind !== "dragging") {
return;
}
model.value.push(new Door(preview, state.door.day));
model.value.push(new Door(preview.value as Rectangle, state.value.door.day));
state = { kind: "idle" };
state.value = { kind: "idle" };
}
function on_mousemove(event: MouseEvent, point: Vector2D) {
if (state.kind === "drawing") {
preview = preview.update(undefined, point);
} else if (state.kind === "dragging") {
const movement = point.minus(state.origin);
preview = state.door.position.move(movement);
if (state.value.kind === "drawing") {
preview.value = preview.value.update(undefined, point);
} else if (state.value.kind === "dragging") {
const movement = point.minus(state.value.origin);
preview.value = state.value.door.position.move(movement);
}
}

View file

@ -32,11 +32,11 @@ const model = defineModel<Door>({ required: true });
const day_input = useTemplateRef("day_input");
const day_str = ref("");
let editing = false;
const editing = ref(false);
function toggle_editing() {
day_str.value = String(model.value.day);
editing = !editing;
editing.value = !editing.value;
}
function on_click(event: MouseEvent) {
@ -44,7 +44,7 @@ function on_click(event: MouseEvent) {
return;
}
if (!editing) {
if (!editing.value) {
const day_input_focus = () => {
if (day_input.value === null) {
nextTick(day_input_focus);
@ -62,7 +62,7 @@ function on_click(event: MouseEvent) {
}
function on_keydown(event: KeyboardEvent) {
if (!editing) {
if (!editing.value) {
return;
}

View file

@ -37,8 +37,3 @@ export function handle_error(error: unknown) {
export function name_door(day: number): string {
return `Türchen ${day}`;
}
export interface Step {
label: string;
icon: string | string[];
}

View file

@ -1,3 +1,4 @@
import { UnwrapRef } from "vue";
import { DoorSaved } from "../model";
import { Rectangle } from "./rectangle";
import { Vector2D } from "./vector2d";
@ -50,3 +51,5 @@ export class Door {
};
}
}
export type VueDoor = UnwrapRef<Door>;

View file

@ -1,3 +1,4 @@
import { UnwrapRef } from "vue";
import { Vector2D } from "./vector2d";
export class Rectangle {
@ -77,3 +78,5 @@ export class Rectangle {
);
}
}
export type VueRectangle = UnwrapRef<Rectangle>;