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:
Elmar Kresse
2024-07-24 10:52:31 +02:00
143 changed files with 12535 additions and 11121 deletions

View File

@@ -104,7 +104,7 @@ const actions = computed(() => [
label: t("calendarLink.toHTWKalendar"),
icon: "pi pi-home",
command: forwardToHTWKalendar,
}
},
]);
</script>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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: &quot;Twemoji Country Flags&quot;,
&quot;Helvetica&quot;, &quot;Comic Sans&quot;, serif;
"
>
{{ displayCountry(slotProps.value) }}
</div>
</div>
<span v-else>
{{ slotProps.placeholder }}

View File

@@ -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>

View File

@@ -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"),

View File

@@ -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;
}),
);