mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2026-01-17 03:22:25 +01:00
Merge branch 'refs/heads/main' into 3-semi-offline-room-finder
# Conflicts: # frontend/src/components/RoomOccupationOffline.vue # frontend/src/model/roomOccupancyList.ts
This commit is contained in:
@@ -21,8 +21,12 @@ import MenuBar from "./components/MenuBar.vue";
|
||||
import { RouteRecordName, RouterView } from "vue-router";
|
||||
import CalendarPreview from "./components/CalendarPreview.vue";
|
||||
import moduleStore from "./store/moduleStore.ts";
|
||||
import { provide, ref } from "vue";
|
||||
import { onMounted, provide, ref } from "vue";
|
||||
import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
|
||||
import settingsStore from "@/store/settingsStore.ts";
|
||||
import { setTheme } from "@/helpers/theme.ts";
|
||||
import { usePrimeVue } from "primevue/config";
|
||||
const primeVue = usePrimeVue();
|
||||
|
||||
const disabledPages = [
|
||||
"room-finder",
|
||||
@@ -33,7 +37,7 @@ const disabledPages = [
|
||||
"edit-calendar",
|
||||
"rooms",
|
||||
"free-rooms",
|
||||
"room-schedule",
|
||||
"room-schedule"
|
||||
];
|
||||
|
||||
const store = moduleStore();
|
||||
@@ -49,14 +53,26 @@ const updateMobile = () => {
|
||||
};
|
||||
|
||||
updateMobile();
|
||||
|
||||
window.addEventListener("resize", updateMobile);
|
||||
|
||||
const settings = settingsStore;
|
||||
const emit = defineEmits(["dark-mode-toggled"]);
|
||||
|
||||
onMounted(() => {
|
||||
// set theme matching browser preference
|
||||
settings().setDarkMode(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
setTheme(settings, primeVue, emit);
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
||||
settings().setDarkMode(e.matches)
|
||||
setTheme(settings, primeVue, emit);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenuBar />
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
<transition name="scale" mode="out-in">
|
||||
<transition mode="out-in" name="scale">
|
||||
<div :key="route.name ?? ''" class="origin-near-top">
|
||||
<component :is="Component" />
|
||||
</div>
|
||||
|
||||
@@ -78,12 +78,13 @@ export async function fetchRoomOccupancy(
|
||||
}
|
||||
|
||||
var roomOccupancyList: RoomOccupancyList = new RoomOccupancyList(
|
||||
new Date(), 0, 0, []
|
||||
new Date(),
|
||||
0,
|
||||
0,
|
||||
[],
|
||||
);
|
||||
|
||||
await fetch(
|
||||
"/api/schedule/rooms?from=" + from_date + "&to=" + to_date,
|
||||
)
|
||||
await fetch("/api/schedule/rooms?from=" + from_date + "&to=" + to_date)
|
||||
.then((response) => {
|
||||
return response.arrayBuffer();
|
||||
})
|
||||
|
||||
@@ -20,4 +20,4 @@ export async function fetchICalendarEvents(token: string): Promise<string> {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ const actions = computed(() => [
|
||||
label: t("calendarLink.toHTWKalendar"),
|
||||
icon: "pi pi-home",
|
||||
command: forwardToHTWKalendar,
|
||||
}
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
<!--
|
||||
Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import { computed, ComputedRef, inject, Ref, ref, watch } from "vue";
|
||||
import { CalendarOptions, DatesSetArg } from "@fullcalendar/core";
|
||||
import {
|
||||
CalendarOptions,
|
||||
DatesSetArg,
|
||||
EventClickArg,
|
||||
} from "@fullcalendar/core";
|
||||
import allLocales from "@fullcalendar/core/locales-all";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
@@ -11,7 +32,7 @@ import iCalenderPlugin from "@fullcalendar/icalendar";
|
||||
import router from "@/router";
|
||||
import { formatYearMonthDay } from "@/helpers/dates.ts";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import tokenStore from "@/store/tokenStore.ts";
|
||||
import { parseICalData } from "@/helpers/ical.ts";
|
||||
import { fetchICalendarEvents } from "@/api/loadICal.ts";
|
||||
@@ -25,21 +46,68 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const selectedToken = computed(() => props.token);
|
||||
const op = ref();
|
||||
const clickedEvent = ref();
|
||||
|
||||
const toggle = (info: EventClickArg) => {
|
||||
const start = !info.event.start ? "" : info.event.start;
|
||||
const end = !info.event.end ? "" : info.event.end;
|
||||
|
||||
if (op.value.visible) {
|
||||
clickedEvent.value = null;
|
||||
op.value.hide();
|
||||
return;
|
||||
} else {
|
||||
clickedEvent.value = {
|
||||
title: info.event._def.title,
|
||||
start: start,
|
||||
end: end,
|
||||
notes: info.event._def.extendedProps.notes,
|
||||
allDay: info.event._def.allDay,
|
||||
location: info.event._def.extendedProps.location,
|
||||
};
|
||||
op.value.show(info.jsEvent);
|
||||
op.value.target = info.el;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const selectedToken = computed(() => {
|
||||
return props.token;
|
||||
});
|
||||
|
||||
const mobilePage = inject("mobilePage") as Ref<boolean>;
|
||||
const date: Ref<Date> = ref(new Date());
|
||||
|
||||
const { data: calendar } = useQuery({
|
||||
const { data: calendar} = useQuery({
|
||||
queryKey: ["userCalendar", selectedToken],
|
||||
queryFn: () =>
|
||||
fetchICalendarEvents(selectedToken.value),
|
||||
queryFn: () => fetchICalendarEvents(selectedToken.value),
|
||||
select: (data) => {
|
||||
return data;
|
||||
},
|
||||
staleTime: 12 * 60 * 60 * 1000, // 12 hours
|
||||
refetchOnWindowFocus: "always",
|
||||
refetchOnReconnect: "always",
|
||||
networkMode: "offlineFirst",
|
||||
enabled: () => tokenStore().token !== "",
|
||||
staleTime: 5000000, // 500 seconds
|
||||
enabled: () => tokenStore().token !== ""
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidateAndRefetchCalendar = () => {
|
||||
console.debug("invalidateAndRefetchCalendar", selectedToken);
|
||||
const queryKey = ["userCalendar", selectedToken];
|
||||
queryClient.invalidateQueries({queryKey: queryKey}).then(() => {
|
||||
queryClient.refetchQueries({queryKey: queryKey});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
invalidateAndRefetchCalendar
|
||||
});
|
||||
|
||||
const events = computed(() => {
|
||||
return parseICalData(calendar.value);
|
||||
});
|
||||
|
||||
const fullCalendar = ref<InstanceType<typeof FullCalendar>>();
|
||||
@@ -58,6 +126,9 @@ const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
},
|
||||
eventClick(info) {
|
||||
toggle(info);
|
||||
},
|
||||
height: "auto",
|
||||
views: {
|
||||
week: {
|
||||
@@ -112,7 +183,7 @@ const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
|
||||
},
|
||||
});
|
||||
},
|
||||
events: parseICalData(calendar.value),
|
||||
events: events.value,
|
||||
}));
|
||||
|
||||
watch(mobilePage, () => {
|
||||
@@ -121,7 +192,22 @@ watch(mobilePage, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
|
||||
<FullCalendar
|
||||
id="overlay-mount-point"
|
||||
ref="fullCalendar"
|
||||
:options="calendarOptions"
|
||||
>
|
||||
</FullCalendar>
|
||||
|
||||
<OverlayPanel ref="op">
|
||||
<div>
|
||||
<h3>{{ clickedEvent.title }}</h3>
|
||||
<p>Location: {{ clickedEvent.location }}</p>
|
||||
<p>Start: {{ clickedEvent.start?.toLocaleString() }}</p>
|
||||
<p>End: {{ clickedEvent.end?.toLocaleString() }}</p>
|
||||
<p>Notes: {{ clickedEvent.notes }}</p>
|
||||
</div>
|
||||
</OverlayPanel>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -130,4 +216,4 @@ watch(mobilePage, () => {
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -17,53 +17,29 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { toggleTheme } from "@/helpers/theme.ts";
|
||||
import settingsStore from "@/store/settingsStore.ts";
|
||||
import { computed } from "vue";
|
||||
import { usePrimeVue } from "primevue/config";
|
||||
|
||||
const PrimeVue = usePrimeVue();
|
||||
const primeVue = usePrimeVue();
|
||||
|
||||
const emit = defineEmits(["dark-mode-toggled"]);
|
||||
|
||||
const isDark = ref(true);
|
||||
const darkTheme = ref("lara-dark-blue"),
|
||||
lightTheme = ref("lara-light-blue");
|
||||
const store = settingsStore;
|
||||
|
||||
function toggleTheme() {
|
||||
isDark.value = !isDark.value;
|
||||
setTheme(isDark.value);
|
||||
}
|
||||
const isDark = computed(() => store().isDark);
|
||||
|
||||
function setTheme(shouldBeDark: boolean) {
|
||||
isDark.value = shouldBeDark;
|
||||
const newTheme = isDark.value ? darkTheme.value : lightTheme.value,
|
||||
oldTheme = isDark.value ? lightTheme.value : darkTheme.value;
|
||||
PrimeVue.changeTheme(oldTheme, newTheme, "theme-link", () => {});
|
||||
emit("dark-mode-toggled", isDark.value);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// set theme matching browser preference
|
||||
setTheme(
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||
);
|
||||
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", (e) => {
|
||||
setTheme(e.matches);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
id="dark-mode-switcher"
|
||||
size="small"
|
||||
class="p-button-rounded w-full md:w-auto"
|
||||
class="p-button-rounded md:w-auto"
|
||||
style="margin-right: 1rem"
|
||||
:severity="isDark ? 'warning' : 'success'"
|
||||
@click="toggleTheme();"
|
||||
@click="toggleTheme(store, primeVue, emit)"
|
||||
>
|
||||
<i v-if="isDark" class="pi pi-sun"></i>
|
||||
<i v-else class="pi pi-moon"></i>
|
||||
|
||||
45
frontend/src/components/DefaultPageSwitcher.vue
Normal file
45
frontend/src/components/DefaultPageSwitcher.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<!--
|
||||
Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ComputedRef, ref } from "vue";
|
||||
import settingsStore from "../store/settingsStore.ts";
|
||||
|
||||
const pageOptions: ComputedRef<(string | {
|
||||
label: string;
|
||||
value: string;
|
||||
})[]> = computed(() => [...settingsStore().getDefaultPageOptions()]);
|
||||
|
||||
const selectedPage = ref(settingsStore().defaultPage);
|
||||
|
||||
function updateDefaultPage(page: { label: string; value: string }) {
|
||||
settingsStore().setDefaultPage(page);
|
||||
}
|
||||
|
||||
updateDefaultPage(settingsStore().defaultPage);
|
||||
</script>
|
||||
<template>
|
||||
<Dropdown
|
||||
v-model="selectedPage"
|
||||
:options="pageOptions"
|
||||
placeholder="Select a Page"
|
||||
class="w-full md:w-14rem"
|
||||
option-label="label"
|
||||
@change="updateDefaultPage($event.value)"
|
||||
></Dropdown>
|
||||
</template>
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import localeStore from "../store/localeStore.ts";
|
||||
import settingsStore from "../store/settingsStore.ts";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { usePrimeVue } from "primevue/config";
|
||||
import primeVue_de from "@/i18n/translations/primevue/prime_vue_local_de.json";
|
||||
@@ -43,7 +43,7 @@ function displayCountry(code: string) {
|
||||
const primeVueConfig = usePrimeVue();
|
||||
|
||||
function updateLocale(locale: string) {
|
||||
localeStore().setLocale(locale);
|
||||
settingsStore().setLocale(locale);
|
||||
|
||||
if (locale === "de") {
|
||||
primeVueConfig.config.locale = primeVue_de;
|
||||
@@ -54,7 +54,7 @@ function updateLocale(locale: string) {
|
||||
}
|
||||
}
|
||||
|
||||
updateLocale(localeStore().locale);
|
||||
updateLocale(settingsStore().locale);
|
||||
</script>
|
||||
<template>
|
||||
<Dropdown
|
||||
@@ -68,7 +68,14 @@ updateLocale(localeStore().locale);
|
||||
<template #value="slotProps">
|
||||
<div v-if="slotProps.value" class="flex align-items-center">
|
||||
<div class="mr-2 flag">{{ displayIcon(slotProps.value) }}</div>
|
||||
<div style="font-family: 'Twemoji Country Flags', 'Helvetica', 'Comic Sans', serif;">{{ displayCountry(slotProps.value) }}</div>
|
||||
<div
|
||||
style="
|
||||
font-family: "Twemoji Country Flags",
|
||||
"Helvetica", "Comic Sans", serif;
|
||||
"
|
||||
>
|
||||
{{ displayCountry(slotProps.value) }}
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>
|
||||
{{ slotProps.placeholder }}
|
||||
|
||||
@@ -17,13 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import LocaleSwitcher from "./LocaleSwitcher.vue";
|
||||
import DarkModeSwitcher from "./DarkModeSwitcher.vue";
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const isDark = ref(true);
|
||||
|
||||
|
||||
const items = computed(() => [
|
||||
{
|
||||
@@ -67,7 +65,7 @@ const items = computed(() => [
|
||||
label: t("roomFinderPage.roomSchedule") + " (offline)",
|
||||
icon: "pi pi-fw pi-ban",
|
||||
route: "/rooms/occupancy/offline",
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -86,13 +84,6 @@ const items = computed(() => [
|
||||
url: "https://www.htwk-leipzig.de/hochschule/kontakt/datenschutzerklaerung/",
|
||||
},
|
||||
]);
|
||||
|
||||
function handleDarkModeToggled(isDarkVar: boolean) {
|
||||
// Do something with isDark value
|
||||
// For example, update the root isDark value
|
||||
// Assuming the root component has an isDark ref
|
||||
isDark.value = isDarkVar;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -140,8 +131,10 @@ function handleDarkModeToggled(isDarkVar: boolean) {
|
||||
</template>
|
||||
<template #end>
|
||||
<div class="flex align-items-stretch justify-content-center">
|
||||
<DarkModeSwitcher @dark-mode-toggled="handleDarkModeToggled"></DarkModeSwitcher>
|
||||
<LocaleSwitcher></LocaleSwitcher>
|
||||
<!-- Settings Button with Gear Icon -->
|
||||
<router-link v-slot="{ navigate }" :to="`/settings`" custom>
|
||||
<Button icon="pi pi-cog" severity="secondary" rounded text size="large" aria-label="Settings" @click="navigate" />
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</Menubar>
|
||||
|
||||
@@ -181,9 +181,7 @@ const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
|
||||
borderColor: event.showFree
|
||||
? "var(--htwk-gruen-600)"
|
||||
: "var(--htwk-grau-60-600)",
|
||||
textColor: event.showFree
|
||||
? "var(--green-50)"
|
||||
: "white",
|
||||
textColor: event.showFree ? "var(--green-50)" : "white",
|
||||
title: event.showFree
|
||||
? t("roomFinderPage.available")
|
||||
: t("roomFinderPage.occupied"),
|
||||
|
||||
@@ -73,18 +73,22 @@ const selectedRoom = computed(() => props.room);
|
||||
|
||||
/**
|
||||
* Transform decoded JSON object with binary data
|
||||
* to anonymized occupancy events
|
||||
* to anonymized occupancy events
|
||||
* @param data RoomOccupancyList with binary data
|
||||
* @returns Anonymized occupancy events
|
||||
*/
|
||||
function transformData(data: RoomOccupancyList) {
|
||||
const events = data
|
||||
.decodeOccupancy(selectedRoom.value, new Date(currentDateFrom.value), new Date(currentDateTo.value))
|
||||
.decodeOccupancy(
|
||||
selectedRoom.value,
|
||||
new Date(currentDateFrom.value),
|
||||
new Date(currentDateTo.value),
|
||||
)
|
||||
.map((event, index) => ({
|
||||
id: index,
|
||||
event: event,
|
||||
}));
|
||||
return events;
|
||||
return events;
|
||||
}
|
||||
|
||||
const { data: occupancy } = useQuery({
|
||||
@@ -185,17 +189,17 @@ const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
|
||||
id: event.id.toString(),
|
||||
start: event.event.start,
|
||||
end: event.event.end,
|
||||
color: event.event.free
|
||||
? "var(--htwk-gruen-500)"
|
||||
color: event.event.free
|
||||
? "var(--htwk-gruen-500)"
|
||||
: "var(--htwk-grau-60-500)",
|
||||
textColor: event.event.free
|
||||
? "var(--green-50)"
|
||||
: "white",
|
||||
? "var(--green-50)"
|
||||
: "white",
|
||||
title: event.event.stub
|
||||
? t("roomFinderPage.stub")
|
||||
: event.event.free
|
||||
? t("roomFinderPage.available")
|
||||
: t("roomFinderPage.occupied"),
|
||||
? t("roomFinderPage.available")
|
||||
: t("roomFinderPage.occupied"),
|
||||
} as EventInput;
|
||||
}),
|
||||
);
|
||||
|
||||
123
frontend/src/helpers/ical.test.ts
Normal file
123
frontend/src/helpers/ical.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//You should have received a copy of the GNU Affero General Public License
|
||||
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { expect, test } from "vitest";
|
||||
import { exportedForTesting } from "@/helpers/ical.ts";
|
||||
import { CalendarComponent } from "ical";
|
||||
|
||||
// colorizeEvents has only the function to colorize the events that are passed to it
|
||||
test("colorizeEventsSameSummary", () => {
|
||||
const events: CalendarComponent[] = [
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Operations Research",
|
||||
},
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Operations Research",
|
||||
},
|
||||
];
|
||||
|
||||
expect(exportedForTesting.colorizeEvents(events)).toEqual([
|
||||
{ summary: "Operations Research", color: "var(--htwk-rot-200)" },
|
||||
{ summary: "Operations Research", color: "var(--htwk-rot-200)" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("colorizeEventsDifferentSummary", () => {
|
||||
const events: CalendarComponent[] = [
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Algorithmische Mathematik",
|
||||
},
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Funktionale Programmierung",
|
||||
},
|
||||
];
|
||||
|
||||
expect(exportedForTesting.colorizeEvents(events)).toEqual([
|
||||
{ summary: "Algorithmische Mathematik", color: "var(--htwk-rot-200)" },
|
||||
{ summary: "Funktionale Programmierung", color: "var(--htwk-gruen-300)" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("filterEventsDistinct", () => {
|
||||
const events: CalendarComponent[] = [
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Operations Research",
|
||||
},
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Operations Research",
|
||||
},
|
||||
];
|
||||
|
||||
expect(exportedForTesting.filterEventsDistinct(events)).toEqual([
|
||||
{ type: "VEVENT", summary: "Operations Research" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("filterEventsDistinctDifferentSummary", () => {
|
||||
const events: CalendarComponent[] = [
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Algorithmische Mathematik",
|
||||
},
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Funktionale Programmierung",
|
||||
},
|
||||
];
|
||||
|
||||
expect(exportedForTesting.filterEventsDistinct(events)).toEqual(events);
|
||||
});
|
||||
|
||||
test("extractedColorizedEvents", () => {
|
||||
const events: CalendarComponent[] = [
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Operations Research",
|
||||
},
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Operations Research",
|
||||
},
|
||||
];
|
||||
|
||||
expect(exportedForTesting.extractedColorizedEvents(events)).toEqual([
|
||||
{ summary: "Operations Research", color: "var(--htwk-rot-200)" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("extractedColorizedEventsDifferentSummary", () => {
|
||||
const events: CalendarComponent[] = [
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Algorithmische Mathematik",
|
||||
},
|
||||
{
|
||||
type: "VEVENT",
|
||||
summary: "Funktionale Programmierung",
|
||||
},
|
||||
];
|
||||
|
||||
expect(exportedForTesting.extractedColorizedEvents(events)).toEqual([
|
||||
{ summary: "Algorithmische Mathematik", color: "var(--htwk-rot-200)" },
|
||||
{ summary: "Funktionale Programmierung", color: "var(--htwk-gruen-300)" },
|
||||
]);
|
||||
});
|
||||
@@ -1,24 +1,138 @@
|
||||
import ICAL from 'ical.js';
|
||||
import { CalendarComponent } from 'ical';
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
export function parseICalData(icalData: string | undefined) {
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//You should have received a copy of the GNU Affero General Public License
|
||||
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import ICAL from "ical.js";
|
||||
import { CalendarComponent } from "ical";
|
||||
|
||||
/**
|
||||
* Interface for the color distinction event
|
||||
* @param title Event name
|
||||
* @param color Color code for the event
|
||||
*/
|
||||
export interface ColorDistinctionEvent {
|
||||
summary: string | undefined;
|
||||
color: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses iCal data and returns an array of calendar components
|
||||
* @param icalData iCal data to parse
|
||||
* @returns Array of calendar components
|
||||
*/
|
||||
export function parseICalData(
|
||||
icalData: string | undefined,
|
||||
): CalendarComponent[] {
|
||||
if (icalData === undefined || !icalData) {
|
||||
return [];
|
||||
} else {
|
||||
const jCalData = ICAL.parse(icalData);
|
||||
const comp = new ICAL.Component(jCalData);
|
||||
const vEvents = comp.getAllSubcomponents("vevent");
|
||||
const events: CalendarComponent[] = vEvents.map((vevent: CalendarComponent) => {
|
||||
return new ICAL.Event(vevent);
|
||||
});
|
||||
const colorDistinctionEvents: ColorDistinctionEvent[] = extractedColorizedEvents(events);
|
||||
|
||||
return vEvents.map((vevent: CalendarComponent) => {
|
||||
const event = new ICAL.Event(vevent);
|
||||
|
||||
return {
|
||||
title: event.summary,
|
||||
start: event.startDate.toJSDate(),
|
||||
end: event.endDate.toJSDate(),
|
||||
notes: event.description,
|
||||
allDay: event.startDate.isDate,
|
||||
color: colorDistinctionEvents.find(
|
||||
(e: ColorDistinctionEvent) => e.summary === event.summary,
|
||||
)?.color,
|
||||
id: event.uid,
|
||||
location: event.location,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const jCalData = ICAL.parse(icalData);
|
||||
const comp = new ICAL.Component(jCalData);
|
||||
const vEvents = comp.getAllSubcomponents('vevent');
|
||||
/**
|
||||
* Extracts the event names and assigns a color to each event
|
||||
* @param vEvents Array of calendar components
|
||||
* @returns Array of objects with event name and color
|
||||
*/
|
||||
function extractedColorizedEvents(
|
||||
vEvents: CalendarComponent[],
|
||||
): ColorDistinctionEvent[] {
|
||||
return colorizeEvents(filterEventsDistinct(vEvents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out duplicate events
|
||||
* @param vEvents Array of calendar components
|
||||
* @returns Array of calendar components without duplicates
|
||||
*/
|
||||
function filterEventsDistinct(
|
||||
vEvents: CalendarComponent[],
|
||||
): CalendarComponent[] {
|
||||
return vEvents.filter(
|
||||
(vevent: CalendarComponent, index: number, self: CalendarComponent[]) => {
|
||||
return (
|
||||
self.findIndex((v) => {
|
||||
return v.summary === vevent.summary;
|
||||
}) === index
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a color to each event
|
||||
* @param vEvents Array of calendar components
|
||||
* @returns Array of objects with event name and color
|
||||
*/
|
||||
function colorizeEvents(vEvents: CalendarComponent[]): ColorDistinctionEvent[] {
|
||||
return vEvents.map((vevent: CalendarComponent) => {
|
||||
const event = new ICAL.Event(vevent);
|
||||
const colors: string[] = [
|
||||
"var(--htwk-rot-200)",
|
||||
"var(--htwk-gruen-300)",
|
||||
"var(--htwk-magenta-400)",
|
||||
"var(--htwk-cyan-400)",
|
||||
"var(--htwk-silbergrau-600)",
|
||||
"var(--htwk-yellow-300)",
|
||||
"var(--htwk-blau-300)",
|
||||
"var(--htwk-dunkelblau-200)",
|
||||
"var(--htwk-rot-400)",
|
||||
"var(--htwk-gruen-400)",
|
||||
"var(--htwk-blau-200)",
|
||||
];
|
||||
|
||||
const randomColor =
|
||||
colors[
|
||||
vEvents.findIndex((e: CalendarComponent) => {
|
||||
return e.summary === vevent.summary;
|
||||
}) % colors.length
|
||||
];
|
||||
|
||||
return {
|
||||
title: event.summary,
|
||||
start: event.startDate.toJSDate(),
|
||||
end: event.endDate.toJSDate(),
|
||||
allDay: event.startDate.isDate,
|
||||
// Include other properties as needed
|
||||
summary: vevent.summary,
|
||||
color: randomColor,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Exported for testing
|
||||
export const exportedForTesting = {
|
||||
extractedColorizedEvents,
|
||||
filterEventsDistinct,
|
||||
colorizeEvents,
|
||||
};
|
||||
|
||||
46
frontend/src/helpers/theme.ts
Normal file
46
frontend/src/helpers/theme.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//You should have received a copy of the GNU Affero General Public License
|
||||
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { ref } from "vue";
|
||||
import { PrimeVueChangeTheme } from "primevue/config";
|
||||
import { EmitFn } from "primevue/ts-helpers";
|
||||
import settingsStore from "@/store/settingsStore.ts";
|
||||
|
||||
const darkTheme = ref("lara-dark-blue"),
|
||||
lightTheme = ref("lara-light-blue");
|
||||
|
||||
export type SettingsStore = typeof settingsStore;
|
||||
|
||||
export function toggleTheme(
|
||||
store: SettingsStore,
|
||||
primeVue: { changeTheme: PrimeVueChangeTheme },
|
||||
emit: EmitFn<"dark-mode-toggled"[]>,
|
||||
): void {
|
||||
store().setDarkMode(!store().isDark);
|
||||
setTheme(store, primeVue, emit);
|
||||
}
|
||||
|
||||
export function setTheme(
|
||||
store: SettingsStore,
|
||||
{ changeTheme }: { changeTheme: PrimeVueChangeTheme },
|
||||
emit: EmitFn<"dark-mode-toggled"[]>
|
||||
) {
|
||||
const isDark = ref(store().isDark);
|
||||
const newTheme = isDark.value ? darkTheme.value : lightTheme.value,
|
||||
oldTheme = isDark.value ? lightTheme.value : darkTheme.value;
|
||||
changeTheme(oldTheme, newTheme, "theme-link", () => { });
|
||||
emit("dark-mode-toggled", isDark.value);
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
||||
//Copyright (C) 2024 HTWKalender support@htwkalender.de
|
||||
|
||||
//This program is free software: you can redistribute it and/or modify
|
||||
//it under the terms of the GNU Affero General Public License as published by
|
||||
//the Free Software Foundation, either version 3 of the License, or
|
||||
//(at your option) any later version.
|
||||
|
||||
//This program is distributed in the hope that it will be useful,
|
||||
//but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
//GNU Affero General Public License for more details.
|
||||
|
||||
//You should have received a copy of the GNU Affero General Public License
|
||||
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const tokenRegex = /^[a-z0-9]{15}$/;
|
||||
const tokenUriRegex = /[?&]token=([a-z0-9]{15})(?:&|$)/;
|
||||
|
||||
@@ -16,4 +32,4 @@ export function extractToken(token: string): string {
|
||||
}
|
||||
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { createI18n } from "vue-i18n";
|
||||
import en from "./translations/en.json";
|
||||
import de from "./translations/de.json";
|
||||
import ja from "./translations/ja.json";
|
||||
import localeStore from "../store/localeStore.ts";
|
||||
import settingsStore from "../store/settingsStore.ts";
|
||||
|
||||
// Private instance of VueI18n object
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -27,7 +27,7 @@ let _i18n: any;
|
||||
function setup() {
|
||||
_i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: localeStore().locale,
|
||||
locale: settingsStore().locale,
|
||||
fallbackLocale: "en",
|
||||
messages: {
|
||||
en,
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
"english": "Englisch",
|
||||
"german": "Deutsch",
|
||||
"japanese": "Japanisch",
|
||||
"notFound": {
|
||||
"headline": "404",
|
||||
"subTitle": "Seite nicht gefunden"
|
||||
},
|
||||
"courseSelection": {
|
||||
"headline": "Willkommen beim HTWKalender",
|
||||
"winterSemester": "Wintersemester",
|
||||
@@ -90,7 +94,8 @@
|
||||
"success": "Erfolg",
|
||||
"error": "Fehler",
|
||||
"successDetail": "Kalender erfolgreich gelöscht",
|
||||
"errorDetail": "Fehler beim Löschen des Kalenders"
|
||||
"errorDetail": "Fehler beim Löschen des Kalenders",
|
||||
"successDetailLoad": "Kalender erfolgreich geladen"
|
||||
}
|
||||
},
|
||||
"additionalModules": {
|
||||
@@ -249,5 +254,12 @@
|
||||
"subTitle": "Hier findest du die Kalenderansicht von deinem persönlichen Feed.",
|
||||
"searchPlaceholder": "Token",
|
||||
"searchButton": "Kalender laden"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "Einstellungen",
|
||||
"subTitle": "Hier kannst du deine Einstellungen bearbeiten.",
|
||||
"language": "Sprache einstellen",
|
||||
"darkMode": "Design auswählen",
|
||||
"defaultPage": "Standardseite"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
"privacy": "privacy",
|
||||
"english": "English",
|
||||
"german": "German",
|
||||
"japanese" : "Japanese",
|
||||
"japanese": "Japanese",
|
||||
"notFound": {
|
||||
"headline": "404",
|
||||
"subTitle": "page not found"
|
||||
},
|
||||
"courseSelection": {
|
||||
"headline": "welcome to HTWKalender",
|
||||
"winterSemester": "winter semester",
|
||||
@@ -90,7 +94,8 @@
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"successDetail": "calendar successfully deleted",
|
||||
"errorDetail": "calendar could not be deleted"
|
||||
"errorDetail": "calendar could not be deleted",
|
||||
"successDetailLoad": "calendar successfully loaded"
|
||||
}
|
||||
},
|
||||
"additionalModules": {
|
||||
@@ -249,5 +254,12 @@
|
||||
"subTitle": "Here you can find the calendar view of your personal feed.",
|
||||
"searchPlaceholder": "calendar token",
|
||||
"searchButton": "load calendar"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "Settings",
|
||||
"subTitle": "Here you can change your settings.",
|
||||
"language": "Choose your language",
|
||||
"darkMode": "Switch page theme",
|
||||
"defaultPage": "Default page"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
"english": "英語",
|
||||
"german": "ドイツ語",
|
||||
"japanese": "日本語",
|
||||
"notFound": {
|
||||
"headline": "404",
|
||||
"subTitle": "ページが見つかりません"
|
||||
},
|
||||
"courseSelection": {
|
||||
"headline": "HTWカレンダーへようこそ",
|
||||
"winterSemester": "冬学期",
|
||||
@@ -90,7 +94,8 @@
|
||||
"success": "成功",
|
||||
"error": "エラー",
|
||||
"successDetail": "カレンダーが正常に削除されました",
|
||||
"errorDetail": "カレンダーを削除できませんでした"
|
||||
"errorDetail": "カレンダーを削除できませんでした",
|
||||
"successDetailLoad": "カレンダーが正常に読み込まれました"
|
||||
}
|
||||
},
|
||||
"additionalModules": {
|
||||
@@ -249,5 +254,12 @@
|
||||
"subTitle": "ここでは、個人のフィードのカレンダー表示を見つけることができます。",
|
||||
"searchPlaceholder": "カレンダートークン",
|
||||
"searchButton": "ロードカレンダー"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "設定",
|
||||
"subTitle": "ここで設定を編集できます。",
|
||||
"language": "言語",
|
||||
"darkMode": "ダークモード",
|
||||
"defaultPage": "デフォルトページ"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,41 +26,15 @@
|
||||
"金曜日",
|
||||
"土曜日"
|
||||
],
|
||||
"dayNamesMin": [
|
||||
"日",
|
||||
"月",
|
||||
"火",
|
||||
"水",
|
||||
"木",
|
||||
"金",
|
||||
"土"
|
||||
],
|
||||
"dayNamesShort": [
|
||||
"日",
|
||||
"月",
|
||||
"火",
|
||||
"水",
|
||||
"木",
|
||||
"金",
|
||||
"土"
|
||||
],
|
||||
"dayNamesMin": ["日", "月", "火", "水", "木", "金", "土"],
|
||||
"dayNamesShort": ["日", "月", "火", "水", "木", "金", "土"],
|
||||
"emptyFilterMessage": "オプションなし",
|
||||
"emptyMessage": "結果なし",
|
||||
"emptySearchMessage": "該当なし",
|
||||
"emptySelectionMessage": "選択なし",
|
||||
"endsWith": "終わる",
|
||||
"equals": "等しい",
|
||||
"fileSizeTypes": [
|
||||
"B",
|
||||
"KB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB",
|
||||
"PB",
|
||||
"EB",
|
||||
"ZB",
|
||||
"YB"
|
||||
],
|
||||
"fileSizeTypes": ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
|
||||
"filter": "フィルター",
|
||||
"firstDayOfWeek": 0,
|
||||
"gt": "超える",
|
||||
|
||||
@@ -31,6 +31,7 @@ import Card from "primevue/card";
|
||||
import DataView from "primevue/dataview";
|
||||
import Dialog from "primevue/dialog";
|
||||
import Slider from "primevue/slider";
|
||||
import OverlayPanel from "primevue/overlaypanel";
|
||||
import ToggleButton from "primevue/togglebutton";
|
||||
import "primeicons/primeicons.css";
|
||||
import "primeflex/primeflex.css";
|
||||
@@ -56,23 +57,23 @@ import Calendar from "primevue/calendar";
|
||||
import i18n from "./i18n";
|
||||
import { VueQueryPlugin } from "@tanstack/vue-query";
|
||||
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
|
||||
polyfillCountryFlagEmojis();
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
app.use(VueQueryPlugin, {
|
||||
queryClientConfig: {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use(PrimeVue);
|
||||
@@ -109,5 +110,6 @@ app.component("ProgressSpinner", ProgressSpinner);
|
||||
app.component("Checkbox", Checkbox);
|
||||
app.component("Skeleton", Skeleton);
|
||||
app.component("Calendar", Calendar);
|
||||
app.component("OverlayPanel", OverlayPanel);
|
||||
|
||||
app.mount("#app");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,13 +16,31 @@
|
||||
|
||||
import { Binary } from "bson";
|
||||
import { AnonymizedOccupancy } from "./event";
|
||||
import { Duration, NormalizedInterval, add, addDays, addMinutes, clamp, differenceInMinutes, eachDayOfInterval, endOfDay, interval, isAfter, isBefore, isEqual, max, min, startOfDay, subDays } from "date-fns";
|
||||
import {
|
||||
Duration,
|
||||
NormalizedInterval,
|
||||
add,
|
||||
addDays,
|
||||
addMinutes,
|
||||
clamp,
|
||||
differenceInMinutes,
|
||||
eachDayOfInterval,
|
||||
endOfDay,
|
||||
interval,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isEqual,
|
||||
max,
|
||||
min,
|
||||
startOfDay,
|
||||
subDays,
|
||||
} from "date-fns";
|
||||
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||
|
||||
/// The start time of the day. 07:00
|
||||
const START_OF_WORKDAY : Duration = {hours: 7};
|
||||
const START_OF_WORKDAY: Duration = { hours: 7 };
|
||||
/// The end time of the day. 20:00
|
||||
const END_OF_WORKDAY : Duration = {hours: 20};
|
||||
const END_OF_WORKDAY: Duration = { hours: 20 };
|
||||
/// The timezone of the data (Leipzig)
|
||||
const TIMEZONE = "Europe/Berlin";
|
||||
|
||||
@@ -32,8 +50,8 @@ const TIMEZONE = "Europe/Berlin";
|
||||
*/
|
||||
class RoomOccupancy {
|
||||
constructor(
|
||||
public name : string,
|
||||
public occupancy : Binary,
|
||||
public name: string,
|
||||
public occupancy: Binary,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -41,22 +59,22 @@ class RoomOccupancy {
|
||||
* Represents the occupancy of multiple rooms.
|
||||
* start is the start date of the occupancy list.
|
||||
* granularity is the duration of a single block in minutes.
|
||||
* blocks is the number of time slices called blocks.
|
||||
* blocks is the number of time slices called blocks.
|
||||
* rooms is a list of RoomOccupancy objects representing the occupancy of each room.
|
||||
*/
|
||||
export class RoomOccupancyList {
|
||||
constructor(
|
||||
public start : Date,
|
||||
public granularity : number,
|
||||
public blocks : number,
|
||||
public rooms : RoomOccupancy[],
|
||||
public start: Date,
|
||||
public granularity: number,
|
||||
public blocks: number,
|
||||
public rooms: RoomOccupancy[],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get a list of all rooms encoded in this occupancy list.
|
||||
* @returns a list of room names.
|
||||
*/
|
||||
public getRooms() : string[] {
|
||||
public getRooms(): string[] {
|
||||
return this.rooms.map((room) => room.name);
|
||||
}
|
||||
|
||||
@@ -67,12 +85,16 @@ export class RoomOccupancyList {
|
||||
* @param to the end of the time range.
|
||||
* @returns a list of AnonymizedEventDTO objects representing the occupancy of the room.
|
||||
*/
|
||||
public decodeOccupancy(room : string, from : Date, to : Date) : AnonymizedOccupancy[] {
|
||||
public decodeOccupancy(
|
||||
room: string,
|
||||
from: Date,
|
||||
to: Date,
|
||||
): AnonymizedOccupancy[] {
|
||||
const roomOccupancy = this.rooms.find((r) => r.name === room);
|
||||
|
||||
// Get start and end of decoded time range (within encoded list and requested range)
|
||||
let decodeInterval = interval(clamp(from, this.getOccupancyInterval()), clamp(to, this.getOccupancyInterval()));
|
||||
|
||||
|
||||
// if the room is not in the list or the time range is empty, return stub events
|
||||
if (roomOccupancy === undefined || isEqual(decodeInterval.start, decodeInterval.end)) {
|
||||
return RoomOccupancyList.generateStubEvents(room, from, to);
|
||||
@@ -82,19 +104,34 @@ export class RoomOccupancyList {
|
||||
|
||||
let {decodeSliceStart, decodeSlice} = this.sliceOccupancy(
|
||||
decodeInterval,
|
||||
roomOccupancy.occupancy.buffer
|
||||
roomOccupancy.occupancy.buffer,
|
||||
);
|
||||
|
||||
// Decode the occupancy data
|
||||
occupancyList.push(...RoomOccupancyList.decodeOccupancyData(new Uint8Array(decodeSlice), decodeSliceStart, this.granularity, room));
|
||||
occupancyList.push(
|
||||
...RoomOccupancyList.decodeOccupancyData(
|
||||
new Uint8Array(decodeSlice),
|
||||
decodeSliceStart,
|
||||
this.granularity,
|
||||
room,
|
||||
),
|
||||
);
|
||||
|
||||
// add stub events for the time before and after the decoded time range
|
||||
if (!isEqual(from, decodeInterval.start)) {
|
||||
occupancyList.push(...RoomOccupancyList.generateStubEvents(room, from, decodeInterval.start));
|
||||
occupancyList.push(
|
||||
...RoomOccupancyList.generateStubEvents(
|
||||
room,
|
||||
from,
|
||||
decodeInterval.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isEqual(to, decodeInterval.end)) {
|
||||
occupancyList.push(...RoomOccupancyList.generateStubEvents(room, decodeInterval.end, to));
|
||||
occupancyList.push(
|
||||
...RoomOccupancyList.generateStubEvents(room, decodeInterval.end, to),
|
||||
);
|
||||
}
|
||||
|
||||
return occupancyList;
|
||||
@@ -107,10 +144,16 @@ export class RoomOccupancyList {
|
||||
* @returns a new occupancy byte array with the starting time of the first byte
|
||||
* @throws an error, if the selected time range is outside of the occupancy list.
|
||||
*/
|
||||
private sliceOccupancy(decodeInterval : NormalizedInterval, occupancy : Uint8Array) : {decodeSliceStart: Date, decodeSlice: Uint8Array} {
|
||||
private sliceOccupancy(
|
||||
decodeInterval: NormalizedInterval,
|
||||
occupancy: Uint8Array,
|
||||
): { decodeSliceStart: Date; decodeSlice: Uint8Array } {
|
||||
// Calculate the slice of bytes, that are needed to decode the requested time range
|
||||
// Note: differenceInMinutes calculates (left - right)
|
||||
let minutesFromStart = differenceInMinutes(decodeInterval.start, this.start);
|
||||
let minutesFromStart = differenceInMinutes(
|
||||
decodeInterval.start,
|
||||
this.start,
|
||||
);
|
||||
let minutesToEnd = differenceInMinutes(decodeInterval.end, this.start);
|
||||
|
||||
let firstByte = Math.floor(minutesFromStart / this.granularity / 8);
|
||||
@@ -118,13 +161,18 @@ export class RoomOccupancyList {
|
||||
|
||||
// check if firstByte and lastByte are within the bounds of the occupancy array and throw an error if not
|
||||
if (
|
||||
firstByte < 0 || firstByte >= occupancy.length ||
|
||||
lastByte < 0 || lastByte > occupancy.length
|
||||
firstByte < 0 ||
|
||||
firstByte >= occupancy.length ||
|
||||
lastByte < 0 ||
|
||||
lastByte > occupancy.length
|
||||
) {
|
||||
throw new Error("Requested time range is outside of the occupancy list.");
|
||||
}
|
||||
|
||||
let decodeSliceStart = addMinutes(this.start, firstByte * 8 * this.granularity);
|
||||
|
||||
let decodeSliceStart = addMinutes(
|
||||
this.start,
|
||||
firstByte * 8 * this.granularity,
|
||||
);
|
||||
let decodeSlice = occupancy.buffer.slice(firstByte, lastByte);
|
||||
|
||||
return { decodeSliceStart, decodeSlice: new Uint8Array(decodeSlice) };
|
||||
@@ -134,8 +182,11 @@ export class RoomOccupancyList {
|
||||
* Get the decoded time interval within the current occupancy list.
|
||||
* @returns the interval of the occupancy list.
|
||||
*/
|
||||
private getOccupancyInterval() : NormalizedInterval<Date> {
|
||||
return interval(this.start, addMinutes(this.start, this.granularity * this.blocks));
|
||||
private getOccupancyInterval(): NormalizedInterval<Date> {
|
||||
return interval(
|
||||
this.start,
|
||||
addMinutes(this.start, this.granularity * this.blocks),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,9 +197,14 @@ export class RoomOccupancyList {
|
||||
* @param room the room name.
|
||||
* @returns a list of AnonymizedOccupancy objects representing the occupancy of the room.
|
||||
*/
|
||||
public static decodeOccupancyData(occupancy : Uint8Array, start : Date, granularity : number, room : string) : AnonymizedOccupancy[] {
|
||||
public static decodeOccupancyData(
|
||||
occupancy: Uint8Array,
|
||||
start: Date,
|
||||
granularity: number,
|
||||
room: string,
|
||||
): AnonymizedOccupancy[] {
|
||||
let occupancyList = [];
|
||||
let firstOccupancyBit : number | null = null;
|
||||
let firstOccupancyBit: number | null = null;
|
||||
|
||||
// Iterate over all bytes that are in the array
|
||||
for (let byte_i = 0; byte_i < occupancy.length; byte_i++) {
|
||||
@@ -156,7 +212,7 @@ export class RoomOccupancyList {
|
||||
|
||||
// Iterate over all bits in the current byte
|
||||
for (let bit_i = 0; bit_i < 8; bit_i++) {
|
||||
let isOccupied = (byte & (1 << (7-bit_i))) !== 0;
|
||||
let isOccupied = (byte & (1 << (7 - bit_i))) !== 0;
|
||||
|
||||
if (firstOccupancyBit === null && isOccupied) {
|
||||
firstOccupancyBit = byte_i * 8 + bit_i;
|
||||
@@ -165,31 +221,35 @@ export class RoomOccupancyList {
|
||||
let endTime = addMinutes(start, (byte_i * 8 + bit_i) * granularity);
|
||||
|
||||
// add event between start and end of a block of boolean true values
|
||||
occupancyList.push(new AnonymizedOccupancy(
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString(),
|
||||
room,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
occupancyList.push(
|
||||
new AnonymizedOccupancy(
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString(),
|
||||
room,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
);
|
||||
|
||||
firstOccupancyBit = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// add last event if it is still ongoing
|
||||
if (firstOccupancyBit !== null) {
|
||||
let startTime = addMinutes(start, firstOccupancyBit * granularity);
|
||||
let endTime = addMinutes(start, occupancy.length * 8 * granularity);
|
||||
|
||||
occupancyList.push(new AnonymizedOccupancy(
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString(),
|
||||
room,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
occupancyList.push(
|
||||
new AnonymizedOccupancy(
|
||||
startTime.toISOString(),
|
||||
endTime.toISOString(),
|
||||
room,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return occupancyList;
|
||||
@@ -198,22 +258,32 @@ export class RoomOccupancyList {
|
||||
/**
|
||||
* Generate a list of AnonymizedOccupancy objects for a given time range.
|
||||
* The generated events are always lying within the time range [START_OF_DAY, END_OF_DAY].
|
||||
*
|
||||
*
|
||||
* @param from The start time within the specified start day.
|
||||
* @param to The end time within the specified end day.
|
||||
* @returns a list of AnonymizedEventDTO objects, from start to end.
|
||||
*/
|
||||
public static generateStubEvents(rooms : string, from : Date, to : Date) : AnonymizedOccupancy[] {
|
||||
public static generateStubEvents(
|
||||
rooms: string,
|
||||
from: Date,
|
||||
to: Date,
|
||||
): AnonymizedOccupancy[] {
|
||||
from = RoomOccupancyList.shiftTimeForwardInsideWorkday(from);
|
||||
to = RoomOccupancyList.shiftTimeBackwardInsideWorkday(to);
|
||||
|
||||
|
||||
if (isAfter(from, to)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return eachDayOfInterval({start: from, end: to}).map((day) => {
|
||||
let startTime = max([from, RoomOccupancyList.setTimeOfDay(day, START_OF_WORKDAY)]);
|
||||
let endTime = min([to, RoomOccupancyList.setTimeOfDay(day, END_OF_WORKDAY)]);
|
||||
return eachDayOfInterval({ start: from, end: to }).map((day) => {
|
||||
let startTime = max([
|
||||
from,
|
||||
RoomOccupancyList.setTimeOfDay(day, START_OF_WORKDAY),
|
||||
]);
|
||||
let endTime = min([
|
||||
to,
|
||||
RoomOccupancyList.setTimeOfDay(day, END_OF_WORKDAY),
|
||||
]);
|
||||
|
||||
return new AnonymizedOccupancy(
|
||||
startTime.toISOString(),
|
||||
@@ -231,26 +301,28 @@ export class RoomOccupancyList {
|
||||
* @param json the JS object to read from.
|
||||
* @returns a RoomOccupancyList object.
|
||||
*/
|
||||
public static fromJSON(json : any) : RoomOccupancyList {
|
||||
public static fromJSON(json: any): RoomOccupancyList {
|
||||
return new RoomOccupancyList(
|
||||
json.start,
|
||||
json.granularity,
|
||||
json.blocks,
|
||||
json.rooms.map((room : any) => new RoomOccupancy(room.name, room.occupancy)
|
||||
));
|
||||
json.rooms.map(
|
||||
(room: any) => new RoomOccupancy(room.name, room.occupancy),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shift the time forward to the start of the next day if it is after the end of the current day.
|
||||
*
|
||||
*
|
||||
* @param date the date time to check if in bounds.
|
||||
* @returns the shifted time.
|
||||
*/
|
||||
private static shiftTimeForwardInsideWorkday(date : Date) : Date {
|
||||
private static shiftTimeForwardInsideWorkday(date: Date): Date {
|
||||
// if the time of date is after the end of the workday
|
||||
if (isAfter(date, RoomOccupancyList.setTimeOfDay(date, END_OF_WORKDAY))) {
|
||||
// shift the time to the start of the next day
|
||||
return RoomOccupancyList.startOfDay(addDays(date,1));
|
||||
return RoomOccupancyList.startOfDay(addDays(date, 1));
|
||||
} else {
|
||||
return date;
|
||||
}
|
||||
@@ -258,15 +330,17 @@ export class RoomOccupancyList {
|
||||
|
||||
/**
|
||||
* Shift the time backward to the end of the previous day if it is before the start of the current day.
|
||||
*
|
||||
*
|
||||
* @param date the date time to check if in bounds.
|
||||
* @returns the shifted time.
|
||||
*/
|
||||
private static shiftTimeBackwardInsideWorkday(date : Date) : Date {
|
||||
private static shiftTimeBackwardInsideWorkday(date: Date): Date {
|
||||
// if the time of date is before the start of the workday
|
||||
if (isBefore(date, RoomOccupancyList.setTimeOfDay(date, START_OF_WORKDAY))) {
|
||||
if (
|
||||
isBefore(date, RoomOccupancyList.setTimeOfDay(date, START_OF_WORKDAY))
|
||||
) {
|
||||
// shift the time to the end of the previous day
|
||||
return RoomOccupancyList.endOfDay(subDays(date,1));
|
||||
return RoomOccupancyList.endOfDay(subDays(date, 1));
|
||||
} else {
|
||||
return date;
|
||||
}
|
||||
@@ -278,16 +352,16 @@ export class RoomOccupancyList {
|
||||
* @param time the time as Duration after 00:00.
|
||||
* @returns new date with changed time values.
|
||||
*/
|
||||
private static setTimeOfDay(date : Date, time : Duration) : Date {
|
||||
private static setTimeOfDay(date: Date, time: Duration): Date {
|
||||
return add(RoomOccupancyList.startOfDay(date), time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start of day in server timezone defined as const TIMEZONE.
|
||||
* @param date
|
||||
* @param date
|
||||
* @returns the start of the day.
|
||||
*/
|
||||
private static startOfDay(date : Date) : Date {
|
||||
private static startOfDay(date: Date): Date {
|
||||
const dateInLocalTimezone = toZonedTime(date, TIMEZONE);
|
||||
return fromZonedTime(startOfDay(dateInLocalTimezone), TIMEZONE);
|
||||
}
|
||||
@@ -297,10 +371,8 @@ export class RoomOccupancyList {
|
||||
* @param date
|
||||
* @returns the end of the day.
|
||||
*/
|
||||
private static endOfDay(date : Date) : Date {
|
||||
private static endOfDay(date: Date): Date {
|
||||
const dateInLocalTimezone = toZonedTime(date, TIMEZONE);
|
||||
return fromZonedTime(endOfDay(dateInLocalTimezone), TIMEZONE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -29,14 +29,23 @@ const EditModules = () => import("../view/editCalendar/EditModules.vue");
|
||||
const CourseSelection = () => import("../view/CourseSelection.vue");
|
||||
const FreeRooms = () => import("../view/FreeRooms.vue");
|
||||
const CalenderViewer = () => import("../view/UserCalendar.vue");
|
||||
const SettingsView = () => import("../view/SettingsView.vue");
|
||||
const NotFound = () => import("../view/NotFound.vue");
|
||||
|
||||
import i18n from "../i18n";
|
||||
import settingsStore from "@/store/settingsStore.ts";
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "default",
|
||||
component: CourseSelection,
|
||||
},
|
||||
{
|
||||
path: "/home",
|
||||
name: "home",
|
||||
component: CourseSelection,
|
||||
},
|
||||
@@ -118,17 +127,35 @@ const router = createRouter({
|
||||
name: "rename-modules",
|
||||
component: RenameModules,
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
name: "settings",
|
||||
component: SettingsView,
|
||||
},
|
||||
{
|
||||
path: "/:catchAll(.*)", // Catch all undefined routes
|
||||
name: "not-found",
|
||||
component: NotFound, // Replace with your NotFound component
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from) => {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const newLocale = to.params.locale;
|
||||
const prevLocale = from.params.locale;
|
||||
// If the locale hasn't changed, do nothing
|
||||
if (newLocale === prevLocale) {
|
||||
return;
|
||||
if (!(newLocale === prevLocale)) {
|
||||
i18n.setLocale(newLocale);
|
||||
}
|
||||
|
||||
const userSettings = settingsStore();
|
||||
const defaultPath = userSettings.defaultPage || "/home";
|
||||
|
||||
if (to.path === "/") {
|
||||
next(defaultPath.value);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
i18n.setLocale(newLocale);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -16,18 +16,54 @@
|
||||
|
||||
import { defineStore } from "pinia";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import router from "@/router";
|
||||
|
||||
const localeStore = defineStore("localeStore", {
|
||||
const settingsStore = defineStore("settingsStore", {
|
||||
state: () => {
|
||||
return {
|
||||
locale: useLocalStorage("locale", "en"), //useLocalStorage takes in a key of 'count' and default value of 0
|
||||
isDark: true,
|
||||
defaultPage: useLocalStorage("defaultPage", {label: "Home", value: "/home"}),
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
setLocale(locale: string) {
|
||||
this.locale = locale;
|
||||
},
|
||||
setDarkMode(isDark: boolean) {
|
||||
this.isDark = isDark;
|
||||
},
|
||||
getDarkMode(): boolean {
|
||||
return this.isDark;
|
||||
},
|
||||
setDefaultPage(page: {
|
||||
label: string;
|
||||
value: string;
|
||||
}) {
|
||||
this.defaultPage = page;
|
||||
},
|
||||
getDefaultPageOptions(): {
|
||||
label: string;
|
||||
value: string;
|
||||
}[] {
|
||||
// get a string array of all the route names
|
||||
const options: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
router.getRoutes().forEach((route) => {
|
||||
if (route.name) {
|
||||
if (typeof route.name === "string") {
|
||||
options.push({
|
||||
label: route.name,
|
||||
value: route.path,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return options;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default localeStore;
|
||||
export default settingsStore;
|
||||
@@ -47,9 +47,9 @@ const hasContent = computed(() => {
|
||||
class="flex align-items-center justify-content-center gap-3 mx-2 mb-4 transition-rolldown"
|
||||
:class="{ 'md:mt-8': hideContent }"
|
||||
>
|
||||
<h3 class="text-4xl">
|
||||
<h1 class="text-4xl">
|
||||
{{ headline }}
|
||||
</h3>
|
||||
</h1>
|
||||
<i v-if="icon" :class="icon" style="font-size: 2rem"></i>
|
||||
</div>
|
||||
<div v-if="subTitle" class="flex justify-content-center">
|
||||
|
||||
@@ -129,8 +129,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
/>
|
||||
</template>
|
||||
<template #body="slotProps">
|
||||
<div class="flex flex-column sm:flex-row justify-content-between flex-1 column-gap-4 mx-2 md:mx-4">
|
||||
<p class="flex-1 align-self-stretch sm:align-self-center my-2">{{ slotProps.data.room }}</p>
|
||||
<div
|
||||
class="flex flex-column sm:flex-row justify-content-between flex-1 column-gap-4 mx-2 md:mx-4"
|
||||
>
|
||||
<p class="flex-1 align-self-stretch sm:align-self-center my-2">
|
||||
{{ slotProps.data.room }}
|
||||
</p>
|
||||
<Button
|
||||
:label="$t('freeRooms.viewOccupancy')"
|
||||
icon="pi pi-hourglass"
|
||||
|
||||
16
frontend/src/view/NotFound.vue
Normal file
16
frontend/src/view/NotFound.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import DynamicPage from "@/view/DynamicPage.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DynamicPage
|
||||
hide-content
|
||||
:headline="$t('notFound.headline')"
|
||||
:sub-title="$t('notFound.subTitle')"
|
||||
>
|
||||
</DynamicPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
78
frontend/src/view/SettingsView.vue
Normal file
78
frontend/src/view/SettingsView.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import LocaleSwitcher from "@/components/LocaleSwitcher.vue";
|
||||
import { ref } from "vue";
|
||||
import DarkModeSwitcher from "@/components/DarkModeSwitcher.vue";
|
||||
import DefaultPageSwitcher from "@/components/DefaultPageSwitcher.vue";
|
||||
|
||||
const icon = "pi pi-cog";
|
||||
const isDark = ref(true);
|
||||
|
||||
function handleDarkModeToggled(isDarkVar: boolean) {
|
||||
// Do something with isDark value
|
||||
// For example, update the root isDark value
|
||||
// Assuming the root component has an isDark ref
|
||||
isDark.value = isDarkVar;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column align-items-center mt-0">
|
||||
<div
|
||||
class="flex align-items-center justify-content-center gap-3 mx-2 mb-4 transition-rolldown md:mt-8"
|
||||
>
|
||||
<h1 class="text-4xl">
|
||||
{{ $t("settings.headline") }}
|
||||
</h1>
|
||||
<i v-if="icon" :class="icon" style="font-size: 2rem"></i>
|
||||
</div>
|
||||
<div v-if="$t('settings.subTitle')" class="flex justify-content-center">
|
||||
<h5 class="text-2xl m-2">{{ $t("settings.subTitle") }}</h5>
|
||||
</div>
|
||||
<div class="flex flex-wrap mx-0 gap-2 my-4 w-full lg:w-8">
|
||||
<slot flex-specs="flex-1 m-0" name="selection"></slot>
|
||||
</div>
|
||||
<div
|
||||
class="opacity-100 transition-all transition-duration-500 transition-ease-in-out w-full lg:w-8"
|
||||
>
|
||||
<div class="flex flex-column justify-content-center">
|
||||
<div class="grid my-2">
|
||||
<div class="col text-center">
|
||||
{{ $t("settings.language") }}
|
||||
</div>
|
||||
<div class="col text-center">
|
||||
<LocaleSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid my-2">
|
||||
<div class="col text-center">
|
||||
{{ $t("settings.defaultPage") }}
|
||||
</div>
|
||||
<div class="col text-center">
|
||||
<DefaultPageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid my-2">
|
||||
<div class="col text-center">
|
||||
{{ $t("settings.darkMode") }}
|
||||
</div>
|
||||
<div class="col text-center">
|
||||
<DarkModeSwitcher
|
||||
@dark-mode-toggled="handleDarkModeToggled"
|
||||
></DarkModeSwitcher>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import CalendarViewer from "@/components/CalendarViewer.vue";
|
||||
import DynamicPage from "@/view/DynamicPage.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
@@ -12,7 +11,7 @@ import tokenStore from "@/store/tokenStore.ts";
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
const toast = useToast();
|
||||
|
||||
const token = ref(tokenStore().token || "" as string );
|
||||
const token = ref(tokenStore().token || ("" as string));
|
||||
|
||||
// parse token from query parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -23,6 +22,8 @@ if (tokenFromUrl) {
|
||||
loadCalendar();
|
||||
}
|
||||
|
||||
const calendarViewerRef = ref<InstanceType<typeof CalendarViewer>>();
|
||||
|
||||
function loadCalendar() {
|
||||
try {
|
||||
token.value = extractToken(token.value);
|
||||
@@ -36,26 +37,33 @@ function loadCalendar() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
moduleStore().removeAllModules();
|
||||
tokenStore().setToken(token.value);
|
||||
|
||||
calendarViewerRef.value?.invalidateAndRefetchCalendar();
|
||||
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: t("editCalendarView.toast.success"),
|
||||
detail: t("editCalendarView.toast.successDetailLoad"),
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (token.value && token.value !== "") {
|
||||
loadCalendar();
|
||||
//loadCalendar();
|
||||
tokenStore().setToken(token.value);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DynamicPage
|
||||
:hide-content="false"
|
||||
:headline="$t('userCalender.headline')"
|
||||
:sub-title="$t('userCalender.subTitle')"
|
||||
>
|
||||
<template #selection="{ flexSpecs }">
|
||||
<template #selection="{ flexSpecs }">
|
||||
<InputText
|
||||
v-model="token"
|
||||
:placeholder="$t('userCalender.searchPlaceholder')"
|
||||
@@ -66,16 +74,12 @@ onMounted(() => {
|
||||
:label="$t('userCalender.searchButton')"
|
||||
icon="pi pi-refresh"
|
||||
@click="loadCalendar()"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<CalendarViewer
|
||||
:token="tokenStore().token"
|
||||
/>
|
||||
</template>
|
||||
</DynamicPage>
|
||||
></Button>
|
||||
</template>
|
||||
<template #content>
|
||||
<CalendarViewer :token="tokenStore().token" ref="calendarViewerRef" />
|
||||
</template>
|
||||
</DynamicPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -34,9 +34,9 @@ async function nextStep() {
|
||||
<template>
|
||||
<div class="flex flex-column align-items-center w-full mb-7">
|
||||
<div class="flex align-items-center justify-content-center m-2">
|
||||
<h3>
|
||||
<h1>
|
||||
{{ $t("additionalModules.subTitle") }}
|
||||
</h3>
|
||||
</h1>
|
||||
</div>
|
||||
<AdditionalModuleTable />
|
||||
<div
|
||||
|
||||
2
frontend/src/vite-env.d.ts
vendored
2
frontend/src/vite-env.d.ts
vendored
@@ -16,4 +16,4 @@
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module 'ical.js';
|
||||
declare module "ical.js";
|
||||
|
||||
Reference in New Issue
Block a user