Merge branch 'main' into 15-calendar-preview

# Conflicts:
#	frontend/src/App.vue
#	frontend/src/view/AdditionalModules.vue
This commit is contained in:
masterelmar
2023-11-20 11:37:25 +01:00
59 changed files with 1539 additions and 811 deletions

View File

@@ -1,11 +1,12 @@
<script lang="ts" setup>
import MenuBar from "./components/MenuBar.vue";
import { RouterView } from "vue-router";
import CalendarPreview from "./components/CalendarPreview.vue";
</script>
<template>
<MenuBar />
<router-view></router-view>
<RouterView />
<CalendarPreview />
</template>

View File

@@ -6,7 +6,13 @@ export async function fetchCourse(): Promise<string[]> {
const courses: string[] = [];
await fetch("/api/courses")
.then((response) => {
return response.json();
//check if response type is json
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
return response.json();
} else {
return [];
}
})
.then((coursesResponse) => {
coursesResponse.forEach((course: string) => courses.push(course));

View File

@@ -3,15 +3,21 @@ import tokenStore from "../store/tokenStore.ts";
import { useToast } from "primevue/usetoast";
import { onMounted } from "vue";
import router from "../router";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const toast = useToast();
const domain = window.location.hostname;
const getLink = () =>
"https://" + domain + "/api/feed?token=" + tokenStore().token;
const show = () => {
toast.add({
severity: "info",
summary: "Info",
detail: "Link copied to clipboard",
summary: t("calendarLink.copyToastSummary"),
detail: t("calendarLink.copyToastNotification"),
life: 3000,
});
};
@@ -27,11 +33,49 @@ function rerouteIfTokenIsEmpty() {
}
function copyToClipboard() {
const text = "https://" + domain + "/api/feed?token=" + tokenStore().token;
// Copy the text inside the text field
navigator.clipboard.writeText(text);
show();
navigator.clipboard.writeText(getLink()).then(show, (err) => {
console.error("Could not copy text: ", err);
toast.add({
severity: "error",
summary: t("calendarLink.copyToastError"),
detail: t("calendarLink.copyToastErrorDetail"),
life: 3000,
});
});
}
const forwardToGoogle = () => {
window.open(
"https://calendar.google.com/calendar/u/0/r?cid=" +
encodeURI(getLink().replace("https://", "http://")),
);
};
const forwardToMicrosoft = () => {
window.open(
"https://outlook.live.com/owa?path=/calendar/action/compose&rru=addsubscription&name=HTWK%20Kalender&url=" +
encodeURI(getLink()),
);
};
const actions = [
{
label: t("calendarLink.copyToClipboard"),
icon: "pi pi-copy",
command: copyToClipboard,
},
{
label: t("calendarLink.toGoogleCalendar"),
icon: "pi pi-google",
command: forwardToGoogle,
},
{
label: t("calendarLink.toMicrosoftCalendar"),
icon: "pi pi-microsoft",
command: forwardToMicrosoft,
},
];
</script>
<template>
@@ -39,11 +83,11 @@ function copyToClipboard() {
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h2>
{{ "https://" + domain + "/api/feed?token=" + tokenStore().token }}
{{ getLink() }}
</h2>
</div>
<div class="flex align-items-center justify-content-center h-4rem m-2">
<Button @click="copyToClipboard">Copy iCal Link</Button>
<div class="flex align-items-center justify-content-center m-2">
<Menu :model="actions" />
</div>
</div>
</template>

View File

@@ -1,11 +1,13 @@
<script lang="ts" setup>
import { Ref, ref } from "vue";
import { computed, ComputedRef, Ref, ref, watch } from "vue";
import {
fetchCourse,
fetchModulesByCourseAndSemester,
} from "../api/fetchCourse";
import ModuleSelection from "./ModuleSelection.vue";
import { Module } from "../model/module.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const courses = async () => {
return await fetchCourse();
@@ -13,15 +15,25 @@ const courses = async () => {
const selectedCourse: Ref<{ name: string }> = ref({ name: "" });
const countries: Ref<{ name: string }[]> = ref([]);
const semesters: Ref<{ name: string; value: string }[]> = ref([
{ name: "Wintersemester", value: "ws" },
{ name: "Sommersemester", value: "ss" },
]);
const semesters: ComputedRef<{ name: string; value: string }[]> = computed(
() => [
{ name: t("courseSelection.winterSemester"), value: "ws" },
{ name: t("courseSelection.summerSemester"), value: "ss" },
],
);
const selectedSemester: Ref<{ name: string; value: string }> = ref(
semesters.value[0],
);
//if semesters get changed, refresh the selected semester ref with a watcher
watch(
semesters,
(newValue: { name: string; value: string }[]) =>
(selectedSemester.value =
newValue[selectedSemester.value.value === "ws" ? 0 : 1]),
);
courses().then(
(data) =>
(countries.value = data.map((course) => {
@@ -43,7 +55,7 @@ async function getModules() {
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3 class="text-4xl">
Welcome to HTWKalender
{{ $t("courseSelection.headline") }}
<i
class="pi pi-calendar vertical-align-baseline"
style="font-size: 2rem"
@@ -53,7 +65,7 @@ async function getModules() {
<div
class="flex align-items-center justify-content-center h-4rem border-round m-2"
>
<h5 class="text-2xl">Please select a course</h5>
<h5 class="text-2xl">{{ $t("courseSelection.subTitle") }}</h5>
</div>
<div
class="flex align-items-center justify-content-center border-round m-2"
@@ -64,7 +76,9 @@ async function getModules() {
class="w-full md:w-25rem mx-2"
filter
option-label="name"
placeholder="Select a Course"
:placeholder="$t('courseSelection.courseDropDown')"
:empty-message="$t('courseSelection.noCoursesAvailable')"
:auto-filter-focus="true"
@change="getModules()"
></Dropdown>
<Dropdown
@@ -72,7 +86,7 @@ async function getModules() {
:options="semesters"
class="w-full md:w-25rem mx-2"
option-label="name"
placeholder="Select a Semester"
:placeholder="$t('courseSelection.semesterDropDown')"
@change="getModules()"
></Dropdown>
</div>

View File

@@ -1,277 +1,141 @@
<script lang="ts" setup></script>
<script lang="ts" setup>
</script>
<template>
<div class="flex align-items-center justify-content-center flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h1>FAQ</h1>
<h1>{{$t('faqView.headline')}}</h1>
</div>
<div class="flex flex-column col-7">
<div class="grid my-2">
<div class="col">
Wie funktioniert das Kalender erstellen mit dem HTWKalender?
{{$t('faqView.firstQuestion')}}
</div>
<div class="col">
Die Webseite ermöglicht es deinen HTWK-Stundenplan in eines deiner
bevorzugten Kalender-Verwaltungs-Programme (Outlook, Google Kalender,
etc.) einzubinden.
{{$t('faqView.firstAnswer')}}
</div>
</div>
<div class="grid my-2">
<div class="col">{{$t('faqView.secondQuestion')}}</div>
<div class="col">
{{$t('faqView.secondAnswer')}}
</div>
</div>
<div class="grid my-2">
<div class="col">Wie genau funktioniert das alles?</div>
<div class="col">
Du wählst deinen Studiengang und das gewünschte Semester aus, danach
kannst du aus den dazugehörigen Modulen wählen. Dabei kannst du
einzelne Module an/abwählen wie "Feiertage" oder Wahlpflichtmodule,
die du nicht belegst. <br />
Im letzten Schritt wird dir der Link mit dem entsprechenden Token für
den von dir erstellenten Kalenders angezeigt. Mit diesem Link kannst
du den Kalender abonnieren oder herunterladen.
</div>
</div>
<div class="grid my-2">
<div class="col">Wie kann ich den Kalender abonnieren?</div>
<div class="col">{{$t('faqView.thirdQuestion')}}</div>
<div class="col">
<Accordion>
<AccordionTab header="Google Calendar">
<AccordionTab :header="$t('faqView.thirdAnswer.tabTitle')">
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>{{$t('faqView.thirdAnswer.google.first')}}</li>
<li>
Bei Google Kalender selbst hast du in der linken Seitenleiste
eine Sektion namens <em>"Weitere Kalender"</em>. Dort klickst
du das kleine Pfeil-Icon rechts neben dem Schritzug. Im
daraufhin erscheinenden Menü gibt es einen Punkt
<em>"Über URL hinzufügen"</em>, den du anklicken musst.
{{$t('faqView.thirdAnswer.google.second')}}
</li>
<li>
Füge den kopierten Kalenderlink ein, klicke auf
<em>"Kalender hinzufügen"</em> und du bist fertig.
{{$t('faqView.thirdAnswer.google.third')}}
</li>
</ol>
</AccordionTab>
<AccordionTab header="Microsoft Outlook">
<p>Unter Outlook 2010:</p>
<AccordionTab :header="$t('faqView.thirdAnswer.microsoft_outlook.title')">
<p>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.title')}}</p>
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>Klicke auf den Reiter <em>Start</em>.</li>
<li>
Dort befindest sich in der Sektion
<em>Kalender verwalten</em> der Button
<em>Kalender öffnen</em>, auf den du kicken musst.
</li>
<li>
Es öffnet sich ein Kontextmenü, in dem du den Punkt
<em>Aus dem Internet</em> anklickst.
</li>
<li>
Im daraufhin erscheinenden Fenster kannst du den Link einfügen
und mit <em>OK</em> bestätigen. Eventuell musst du das
Abonnement in einem weiteren Schritt noch bestätigen. Dann
bist du fertig.
</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.first')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.second')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.third')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.fourth')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2010.fifth')}}</li>
</ol>
<p>Unter Outlook 2007:</p>
<p>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.title')}}</p>
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>
Unter <em>Extras</em> findest du die
<em>Kontoeinstellungen</em>.
</li>
<li>
Dort auf den Reiter <em>Internetkalender</em> klicken.
</li>
<li>Auf <em>Neu</em> klicken.</li>
<li>
Im sich öffnenden Fenster musst du den Link einfügen.
Allerdings musst du <em>http://”</em> durch
<em>webcal://”</em> austauschen.
</li>
<li>
Spätestens beim nächsten Start sollte der Kalender
aktualisiert werden und dir fortan zur Verfügung stehen.
</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.first')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.second')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.third')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.fourth')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.fifth')}}</li>
<li>{{$t('faqView.thirdAnswer.microsoft_outlook.outlook_2007.sixth')}}</li>
</ol>
</AccordionTab>
<AccordionTab header="Kalender (OS X)">
<AccordionTab :header="$t('faqView.thirdAnswer.apple_osx.title')">
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>
Unter <em>Ablage</em> findest du den Punkt
<em>Neues Kalenderabonnement</em> (oder unter Snow Leopard
<em>Abonnieren</em>).
</li>
<li>
Im daraufhin erscheinenden Fenster musst du den kopierten Link
einfügen und <em>abonnieren</em> klicken.
</li>
<li>
Anschließend kannst du dem Kalender noch einen Namen geben und
bestimmen, wie oft er aktualisiert werden soll. Falls du
iCloud auf deinem iPhone o.ä. verwendest, empfehle ich dir bei
<em>Ort</em> unbedingt <em>iCloud</em> zu wählen. So hast
du deinen Stundenplan ohne weiteres zutun auch unterwegs immer
parat.
</li>
<li>{{$t('faqView.thirdAnswer.apple_osx.first')}}</li>
<li>{{$t('faqView.thirdAnswer.apple_osx.second')}}</li>
<li>{{$t('faqView.thirdAnswer.apple_osx.third')}}</li>
<li>{{$t('faqView.thirdAnswer.apple_osx.fourth')}}</li>
</ol>
</AccordionTab>
<AccordionTab header="Thunderbird">
<AccordionTab :header="$t('faqView.thirdAnswer.thunderbird.title')">
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>
Im Menü <em>Termine und Aufgaben</em> den Punkt
<em>Kalender</em> wählen.
</li>
<li>
Links siehst du die Kalenderübersicht. In diesem Bereich über
die rechte Maustaste klicken und im darauf erscheinenden
Kontextmenü <em>Neuer Kalender</em> anklicken.
</li>
<li>
Du hast die Wahl zwischen <em>Auf meinem Computer</em> und
<em>Im Netzwerk</em>. Bitte letzteres wählen und
<em>Fortsetzen</em> klicken.
</li>
<li>
Im folgenden Fenster lässt du das <em>Format</em> wie es ist
(<em>iCalender</em>).
</li>
<li>
Unter <em>"Adresse"</em> den kopierten Kalenderlink einfügen.
</li>
<li>
Anschließend kannst du noch einen Namen vergeben und weitere
Einstellungen nach Belieben vornehmen.
</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.one') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.two') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.three') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.four') }} </li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.five') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.six') }}</li>
<li>{{ $t('faqView.thirdAnswer.thunderbird.seven') }}</li>
</ol>
</AccordionTab>
<AccordionTab header="IPhone">
<p>
Der einfachste Weg unter iOS ist der iCloud-Sync (siehe
Anleitung für OS X Kalender). Hier ist der andere Weg:
</p>
<AccordionTab :header="$t('faqView.thirdAnswer.iphone.title')">
<p>{{$t('faqView.thirdAnswer.iphone.description')}}</p>
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>Gehe in die <em>Systemeinstellungen</em>.</li>
<li>Dort wählst du <em>Mail, Kontakte, Kalender</em>.</li>
<li><em>Account hinzufügen</em> auswählen.</li>
<li>Ganz unten tippst du auf <em>Andere</em>.</li>
<li>
Der letzte Punkt ist <em>Kalenderabo hinzufügen</em>. Dort
drauftippen.
</li>
<li>
Im daraufhin erscheinenden Textfeld fügst du den Kalenderlink
ein und drückst oben auf <em>weiter</em>.
</li>
<li>
Du kannst noch eine <em>Beschreibung</em> vergeben. Den Rest
solltest du lassen, wie er ist.
</li>
<li>
Nach kurzer Zeit taucht der abonnierte Kalender in der
Kalender-App auf.
</li>
<li>{{$t('faqView.thirdAnswer.iphone.one')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.two')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.three')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.four')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.five')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.six')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.seven')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.eight')}}</li>
<li>{{$t('faqView.thirdAnswer.iphone.nine')}}</li>
</ol>
</AccordionTab>
<AccordionTab header="Android">
<p>
Unter Android ist die Synchronisierung mit dem Google Kalender
die einfachste Variante. Schaue bitte in die Google Kalender
Anleitung um zu erfahren, wie du den Kalender dort abonnierst.
</p>
<p>{{$t('faqView.thirdAnswer.android.description')}}</p>
</AccordionTab>
<AccordionTab header="Windows Phone">
<p>
Am einfachsten ist unter Windows Phone die Synchronisierung über
Outlook.com:
</p>
<p>{{$t('faqView.thirdAnswer.windows_phone.description')}}</p>
<ol>
<li>Erstelle deinen Kalender und kopiere den Link.</li>
<li>Bei Outlook.com anmelden und in den Kalender wechseln.</li>
<li>Auf <em>Subscribe</em> klicken.</li>
<li><em>Subscribe to a public calendar</em> auswählen.</li>
<li>Bei <em>Calendar URL</em> den Kalenderlink einfügen.</li>
<li>Sonstige Einstellungen nach Belieben vornehmen.</li>
<li>Auf <em>Subscribe to calendar</em> klicken.</li>
<li>
Das Windows-Phone-Gerät muss mit dem gleichen
Outlook.com-Benutzerkonto angemeldet sein. Fortan sollte die
Synchronisierung des Kalenders automatisch erfolgen.
</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.one')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.two')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.three')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.four')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.five')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.six')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.seven')}}</li>
<li>{{$t('faqView.thirdAnswer.windows_phone.eight')}}</li>
</ol>
</AccordionTab>
</Accordion>
</div>
</div>
<div class="grid my-2">
<div class="col">
Kalender abonnieren? Ich will den <em>downloaden</em>!
</div>
<div class="col">
Das kannst du gern tun. Nachdem dein persönlicher Stundenplan erstellt
wurde, hast du die Möglichkeit ihn herunterzuladen. Außerdem kannst du
ihn jederzeit herunterladen, wenn du den generierten Link einfach in
deinem Browser aufrufst. <br />
Bedenke hierbei, dass heruntergeladene Kalender bzw. Stundenpläne sich
nicht aktualisieren werden. Das ist nur möglich, wenn du den Kalender
abonnierst.
</div>
<div class="col">{{$t('faqView.fourthQuestion')}}</div>
<div class="col">{{$t('faqView.fourthAnswer')}}</div>
</div>
<div class="grid my-2">
<div class="col">
Ich belege zusätzlich Module aus anderen Studiengängen und möchte
diese auch in meinem Stundenplan haben.
</div>
<div class="col">
Nachdem du die Möglichkeit hattest, die für deinen Studiengang
vorgesehenen Module aus dem Modulhandbuch auszuwählen, wirst du auf
eine zweite Seite weitergeleitet. Dort hast du die Möglichkeit,
weitere Module aus anderen Studiengängen in deinen Studienplan
einzufügen.
</div>
<div class="col">{{$t('faqView.fifthQuestion')}}</div>
<div class="col">{{$t('faqView.fifthAnswer')}}</div>
</div>
<div class="grid my-2">
<div class="col my-2">Aktualisierung Probleme mit dem Kalender?</div>
<div class="col">
Das liegt vermutlich daran, dass du ihn heruntergeladen statt
abonniert hast. Automatisch aktualisieren können sich nur Kalender,
die du abonniert hast. Eine Aktualisierung des Kalenders auf dem
Server wird täglich um 4:00 Uhr durchgeführt. So wird gewährleistet
das alle Veränderungen seitens der HTWK übernommen werden.
</div>
<div class="col my-2">{{$t('faqView.sixthQuestion')}}</div>
<div class="col">{{$t('faqView.sixthAnswer')}}</div>
</div>
<div class="grid my-2">
<div class="col">
Wie lange ist mein Stundenplan bzw. der Link dorthin gültig?
</div>
<div class="col">
Studenpläne sind erstmal nur für die ausgewählten Semester gültig. Da
durch Wahlpflichtmodule oder deine Planung sich Veränderungen ergeben
können.
</div>
<div class="col">{{$t('faqView.seventhQuestion')}}</div>
<div class="col">{{$t('faqView.seventhAnswer')}}</div>
</div>
<div class="grid my-2">
<div class="col">Preis und Entwicklung?</div>
<div class="col">
Die Kosten können durch das selbständiges Hosting vollständig
ausgelagert werden. Die Entwicklung soll als aktives Git Projekt auch
durch die Community verwaltet werden.
</div>
<div class="col">{{$t('faqView.eighthQuestion')}}</div>
<div class="col">{{$t('faqView.eighthAnswer')}}</div>
</div>
<p>
Nicht gefunden, wonach du suchst?<br />
<a href="/imprint">Kontakt aufnehmen</a>
{{$t('faqView.notFound')}}<br />
<a href="/imprint">{{$t('faqView.contact')}}</a>
</p>
</div>
</div>

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { computed } from "vue";
import localeStore from "../store/localeStore.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const countries = computed(() => [
{ name: t("english"), code: "en", icon: "🇬🇧" },
{ name: t("german"), code: "de", icon: "🇩🇪" },
]);
function displayIcon(code: string) {
return countries.value.find((country) => country.code === code)?.icon;
}
function displayCountry(code: string) {
return countries.value.find((country) => country.code === code)?.name;
}
function updateLocale(locale: string) {
localeStore().setLocale(locale);
}
</script>
<template>
<Dropdown
v-model="$i18n.locale"
:options="$i18n.availableLocales"
option-label="name"
placeholder="Select a Language"
class="w-full md:w-14rem"
@change="updateLocale($event.data)"
>
<template #value="slotProps">
<div v-if="slotProps.value" class="flex align-items-center">
<div class="mr-2 flag">{{ displayIcon(slotProps.value) }}</div>
<div>{{ displayCountry(slotProps.value) }}</div>
</div>
<span v-else>
{{ slotProps.placeholder }}
</span>
</template>
<template #option="slotProps">
<div class="flex align-items-center">
<div class="mr-2 flag">{{ displayIcon(slotProps.option) }}</div>
<div>{{ displayCountry(slotProps.option) }}</div>
</div>
</template>
</Dropdown>
</template>

View File

@@ -1,43 +1,69 @@
<script lang="ts" setup>
import { ref } from "vue";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import LocaleSwitcher from "./LocaleSwitcher.vue";
const { t } = useI18n({ useScope: "global" });
const items = ref([
const items = computed(() => [
{
label: "Create Calendar",
label: t("createCalendar"),
icon: "pi pi-fw pi-plus",
url: "/",
route: "/",
},
{
label: "Edit Calendar",
label: t("editCalendar"),
icon: "pi pi-fw pi-pencil",
url: "/edit",
route: "/edit",
},
{
label: "Check Room Availability",
label: t("roomFinder"),
icon: "pi pi-fw pi-calendar",
url: "/rooms",
route: `rooms`,
},
{
label: "FAQ",
label: t("faq"),
icon: "pi pi-fw pi-book",
url: "/faq",
route: `faq`,
},
{
label: "Imprint",
label: t("imprint"),
icon: "pi pi-fw pi-id-card",
url: "/imprint",
route: `imprint`,
},
{
label: "Privacy",
url: "/privacy-policy",
label: t("privacy"),
icon: "pi pi-fw pi-exclamation-triangle",
route: `privacy-policy`,
},
]);
</script>
<template>
<Menubar :model="items" class="menubar justify-content-center">
<template #start></template>
<Menubar :model="items" class="menubar justify-content-between">
<template #start>
<img
width="35"
height="40"
src="../../public/htwk.svg"
alt="Logo"
class="h-10 w-10 mr-6"
style="margin-left: 1rem"
/>
</template>
<template #item="{ item }">
<router-link v-slot="{ navigate }" :to="item.route" custom>
<Button
:label="String(item.label)"
:icon="item.icon"
severity="secondary"
text
@click="navigate"
/>
</router-link>
</template>
<template #end>
<LocaleSwitcher></LocaleSwitcher>
</template>
</Menubar>
</template>

View File

@@ -1,10 +1,30 @@
<script lang="ts" setup>
import { inject } from "vue";
import { Module } from "../model/module.ts";
import {inject} from "vue";
import {Module} from "../model/module.ts";
import {Event} from "../model/event.ts";
import moment from "moment-timezone";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dialogRef = inject("dialogRef") as any;
const module = dialogRef.value.data.module as Module;
// formats 2023-10-26 11:45:00.000Z to DD-MM-YYYY HH:MM
function formatTimestamp(timestampString: string): string {
// Den übergebenen Zeitstempel in ein Moment-Objekt umwandeln
const timestamp = moment(timestampString);
// Die Zeitzone auf "Europe/Berlin" setzen
const berlinTime = timestamp.tz('Europe/Berlin');
// Das gewünschte Format für die Ausgabe festlegen
return berlinTime.format('DD.MM.YYYY HH:mm');
}
function sortModuleEventsByStart(events: Event[]) {
return events.sort((a, b) => {
return a.start.localeCompare(b.start);
});
}
</script>
<template>
@@ -12,24 +32,32 @@ const module = dialogRef.value.data.module as Module;
<h2>{{ module.name }}</h2>
<table>
<tr>
<td>Course: {{ module.course }}</td>
<td>{{ $t("moduleInformation.course") }}: {{ module.course }}</td>
</tr>
<tr>
<td>Person: {{ module.prof }}</td>
<td>{{ $t("moduleInformation.person") }}: {{ module.prof }}</td>
</tr>
<tr>
<td>Semester: {{ module.semester }}</td>
<td>{{ $t("moduleInformation.semester") }}: {{ module.semester }}</td>
</tr>
<tr>
<td>
<div class="card">
<DataTable :value="module.events" table-style="min-width: 50rem">
<Column field="day" header="Day"></Column>
<Column field="start" header="Start"></Column>
<Column field="end" header="End"></Column>
<Column field="rooms" header="Room"></Column>
<Column field="eventType" header="Type"></Column>
<Column field="week" header="Week"></Column>
<DataTable :value="sortModuleEventsByStart(module.events)" table-style="min-width: 50rem">
<Column field="day" :header="$t('moduleInformation.day')"></Column>
<Column field="start" :header="$t('moduleInformation.start')">
<template #body="slotProps">
{{ formatTimestamp(slotProps.data.start) }}
</template>
</Column>
<Column field="end" :header="$t('moduleInformation.end')">
<template #body="slotProps">
{{formatTimestamp( slotProps.data.end) }}
</template>
</Column>
<Column field="rooms" :header="$t('moduleInformation.room')"></Column>
<Column field="eventType" :header="$t('moduleInformation.type')"></Column>
<Column field="week" :header="$t('moduleInformation.week')"></Column>
</DataTable>
</div>
</td>

View File

@@ -64,7 +64,7 @@ function nextStep() {
:disabled="selectedModules.length < 1"
class="col-4 justify-content-center"
@click="nextStep()"
>Next Step
>{{ $t("moduleSelection.nextStep") }}
</Button>
</div>
<div class="flex align-items-center justify-content-center">
@@ -72,16 +72,19 @@ function nextStep() {
<template #header>
<div class="flex justify-content-between flex-wrap">
<div class="flex align-items-center justify-content-center">
<h3>Modules - {{ selectedModules.length }}</h3>
<h3>
{{ $t("moduleSelection.modules") }} -
{{ selectedModules.length }}
</h3>
</div>
<div class="flex align-items-center justify-content-center">
<ToggleButton
v-model="allSelected"
class="w-12rem"
off-icon="pi pi-times"
off-label="Unselect All"
:off-label="$t('moduleSelection.deselectAll')"
on-icon="pi pi-check"
on-label="Select All"
:on-label="$t('moduleSelection.selectAll')"
@click="selectAllModules(!allSelected)"
/>
</div>
@@ -89,7 +92,7 @@ function nextStep() {
</template>
<template #empty>
<p class="p-4 text-2xl font-bold text-900 empty-message">
No Modules found for this course
{{ $t("moduleSelection.noModulesAvailable") }}
</p>
</template>
<template #list="slotProps">
@@ -112,9 +115,9 @@ function nextStep() {
v-model="modulesWithSelection[slotProps.index].selected"
class="w-9rem"
off-icon="pi pi-times"
off-label="Unselected"
:off-label="$t('moduleSelection.unselected')"
on-icon="pi pi-check"
on-label="Selected"
:on-label="$t('moduleSelection.selected')"
/>
</div>
</div>

View File

@@ -1,18 +1,30 @@
<script setup lang="ts">
import { Ref, ref } from "vue";
import { computed, Ref, ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const helpVisible: Ref<boolean> = ref(false);
const placeholders = ref([
const placeholders = computed(() => [
{
placeholder: "%t",
description: "Event Type",
examples: "V = Vorlesung, S = Seminar, P = Praktikum/Prüfung",
description: t("moduleTemplateDialog.eventTyp"),
examples:
"V = " +
t("moduleTemplateDialog.lecture") +
", S = " +
t("moduleTemplateDialog.seminar") +
", P = " +
t("moduleTemplateDialog.exam"),
},
{
placeholder: "%p",
description: "Mandatory",
examples: "w = optional, p = mandatory",
description: t("moduleTemplateDialog.mandatory"),
examples:
"w = " +
t("moduleTemplateDialog.optional") +
", p = " +
t("moduleTemplateDialog.mandatory"),
},
]);
</script>
@@ -28,20 +40,30 @@ const placeholders = ref([
aria-label="Help"
@click="helpVisible = true"
/>
<Dialog v-model:visible="helpVisible" header="Module configuration">
<Dialog
v-model:visible="helpVisible"
:header="$t('moduleTemplateDialog.moduleConfiguration')"
>
<p>
Here you can rename your modules to your liking. This will be the name of
the event in your calendar.
{{ t("moduleTemplateDialog.explanationOne") }}
</p>
<p>You can use the following placeholders in your module names:</p>
<p>{{ t("moduleTemplateDialog.tableDescription") }}</p>
<DataTable :value="placeholders">
<Column field="placeholder" header="Placeholder"></Column>
<Column field="description" header="Description"></Column>
<Column field="examples" header="Examples"></Column>
<Column
field="placeholder"
:header="$t('moduleTemplateDialog.placeholder')"
></Column>
<Column
field="description"
:header="$t('moduleTemplateDialog.description')"
></Column>
<Column
field="examples"
:header="$t('moduleTemplateDialog.examples')"
></Column>
</DataTable>
<p>
Additionally, you can toggle notifications for each module. If you do so,
you will be notified 15 minutes before the event starts.
{{ t("moduleTemplateDialog.explanationTwo") }}
</p>
</Dialog>
</template>

View File

@@ -3,8 +3,11 @@ import moduleStore from "../store/moduleStore.ts";
import { createIndividualFeed } from "../api/createFeed.ts";
import router from "../router";
import tokenStore from "../store/tokenStore.ts";
import { ref } from "vue";
import { computed, ref } from "vue";
import ModuleTemplateDialog from "./ModuleTemplateDialog.vue";
import { onlyWhitespace } from "../helpers/strings.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const store = moduleStore();
const tableData = ref(
@@ -16,10 +19,10 @@ const tableData = ref(
}),
);
const columns = ref([
{ field: "Course", header: "Course" },
{ field: "Module", header: "Module" },
{ field: "Reminder", header: "Reminder" },
const columns = computed(() => [
{ field: "Course", header: t("moduleInformation.course") },
{ field: "Module", header: t("moduleInformation.module") },
{ field: "Reminder", header: t("renameModules.reminder") },
]);
async function finalStep() {
@@ -32,7 +35,7 @@ async function finalStep() {
<template>
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3>Configure your selected Modules to your liking.</h3>
<h3>{{ $t("renameModules.subTitle") }}</h3>
<ModuleTemplateDialog />
</div>
<div class="card flex align-items-center justify-content-center m-2">
@@ -44,7 +47,7 @@ async function finalStep() {
>
<template #header>
<div class="flex align-items-center justify-content-end">
Enable all notifications:
{{ $t("renameModules.enableAllNotifications") }}
<InputSwitch
class="mx-4"
:model-value="
@@ -69,7 +72,11 @@ async function finalStep() {
<!-- Text Body -->
<template #body="{ data, field }">
<template v-if="field === 'Module'">
{{ data[field].userDefinedName }}
{{
onlyWhitespace(data[field].userDefinedName)
? data[field].name
: data[field].userDefinedName
}}
</template>
<template v-else-if="field === 'Reminder'">
<Button
@@ -95,10 +102,6 @@ async function finalStep() {
/>
</template>
<template v-else-if="field === 'Reminder'">
<!--<InputSwitch
v-model="data.Module.reminder"
class="align-self-center"
/>-->
<Button
:icon="data.Module.reminder ? 'pi pi-bell' : 'pi pi-times'"
:severity="data.Module.reminder ? 'warning' : 'secondary'"
@@ -117,7 +120,7 @@ async function finalStep() {
</div>
<div class="flex align-items-center justify-content-center h-4rem m-2">
<Button @click="finalStep()">Next Step</Button>
<Button @click="finalStep()">{{ $t("renameModules.nextStep") }}</Button>
</div>
</div>
</template>

View File

@@ -3,9 +3,11 @@ import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
import { computed, ref, Ref, watch } from "vue";
import { computed, ComputedRef, ref, Ref, watch } from "vue";
import { CalendarOptions, EventInput } from "@fullcalendar/core";
import { fetchEventsByRoomAndDuration } from "../api/fetchRoom.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const props = defineProps({
room: {
@@ -53,84 +55,92 @@ async function getOccupation() {
calendar?.refetchEvents();
}
const calendarOptions: CalendarOptions = {
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
initialView: "week",
dayHeaderFormat: { weekday: "short", omitCommas: true },
slotDuration: "00:15:00",
eventTimeFormat: {
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
views: {
week: {
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
},
dateAlignment: "week",
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
import allLocales from "@fullcalendar/core/locales-all";
duration: { days: 7 },
firstDay: 1,
allDaySlot: false,
hiddenDays: [0],
const calendarOptions: ComputedRef<CalendarOptions> = computed(() => {
return {
locales: allLocales,
locale: t("languageCode"),
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
initialView: "week",
dayHeaderFormat: { weekday: "short", omitCommas: true },
slotDuration: "00:15:00",
eventTimeFormat: {
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
Day: {
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
views: {
week: {
description: "Wochenansicht",
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
},
dateAlignment: "week",
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
duration: { days: 7 },
firstDay: 1,
allDaySlot: false,
hiddenDays: [0],
},
Day: {
type: "timeGrid",
slotLabelFormat: {
hour: "numeric",
minute: "2-digit",
omitZeroMinute: false,
meridiem: false,
hour12: false,
},
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
duration: { days: 1 },
allDaySlot: false,
hiddenDays: [0],
},
titleFormat: { month: "short", day: "numeric" },
slotMinTime: "06:00:00",
slotMaxTime: "22:00:00",
duration: { days: 1 },
allDaySlot: false,
hiddenDays: [0],
},
},
headerToolbar: {
end: "prev,next today",
center: "title",
start: "week,Day",
},
headerToolbar: {
end: "prev,next today",
center: "title",
start: "week,Day",
},
datesSet: function (dateInfo) {
const view = dateInfo.view;
const offset = new Date().getTimezoneOffset();
const startDate = new Date(view.activeStart.getTime() - offset * 60 * 1000);
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
currentDateFrom.value = startDate.toISOString().split("T")[0];
currentDateTo.value = endDate.toISOString().split("T")[0];
getOccupation();
},
events: function (_info, successCallback, failureCallback) {
if (occupations.value.length === 0) {
failureCallback(new Error("no events"));
} else {
successCallback(
occupations.value.map((event) => {
return {
id: event.id.toString(),
start: event.start,
end: event.end,
} as EventInput;
}),
datesSet: function (dateInfo: any) {
const view = dateInfo.view;
const offset = new Date().getTimezoneOffset();
const startDate = new Date(
view.activeStart.getTime() - offset * 60 * 1000,
);
}
},
};
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
currentDateFrom.value = startDate.toISOString().split("T")[0];
currentDateTo.value = endDate.toISOString().split("T")[0];
getOccupation();
},
events: function (_info: any, successCallback: any, failureCallback: any) {
if (occupations.value.length === 0) {
failureCallback(new Error("no events"));
} else {
successCallback(
occupations.value.map((event) => {
return {
id: event.id.toString(),
start: event.start,
end: event.end,
} as EventInput;
}),
);
}
},
};
});
</script>
<template>
<FullCalendar ref="fullCalendar" :options="calendarOptions" />

View File

@@ -91,6 +91,7 @@ function selectChange() {
class="custom-multiselect"
filter
placeholder="Select additional modules"
:auto-filter-focus="true"
@change="selectChange()"
@selectall-change="onSelectAllChange($event)"
>

View File

@@ -7,6 +7,7 @@ import { saveIndividualFeed } from "../../api/createFeed";
import tokenStore from "../../store/tokenStore";
import router from "../../router";
import ModuleTemplateDialog from "../ModuleTemplateDialog.vue";
import { onlyWhitespace } from "../../helpers/strings.ts";
const store = moduleStore();
const tableData = computed(() =>
@@ -90,7 +91,11 @@ async function finalStep() {
<!-- Text Body -->
<template #body="{ data, field }">
<template v-if="field === 'Module'">
{{ data[field].userDefinedName }}
{{
onlyWhitespace(data[field].userDefinedName)
? data[field].name
: data[field].userDefinedName
}}
</template>
<template v-else-if="field === 'Reminder'">
<Button
@@ -125,6 +130,9 @@ async function finalStep() {
@click="data.Module.reminder = !data.Module.reminder"
></Button>
</template>
<template v-else>
{{ data[field] }}
</template>
</template>
</Column>
<Column>

View File

@@ -0,0 +1,8 @@
/**
* Returns false only if the input argument is a string which contains characters that are not considered a whitespace
* @param str - The string to check for whitespace
* @returns boolean - true if the string contains only whitespace, false otherwise
*/
export function onlyWhitespace(str: string): boolean {
return !str || str.length === 0 || /^\s*$/.test(str);
}

View File

@@ -0,0 +1,46 @@
import { createI18n } from "vue-i18n";
import en from "./translations/en.json";
import de from "./translations/de.json";
import localeStore from "../store/localeStore.ts";
export const supportedLocales = {
en: { name: "English" },
de: { name: "Deutsch" },
};
// Private instance of VueI18n object
let _i18n: any;
// Initializer
function setup() {
_i18n = createI18n({
legacy: false,
locale: localeStore().locale,
fallbackLocale: "en",
messages: {
en,
de,
},
});
return _i18n;
}
// Sets the active locale.
function setLocale(newLocale: any) {
_i18n.global.locale = newLocale;
setDocumentAttributesFor(newLocale);
}
function setDocumentAttributesFor(locale: any) {
const htmlElement = document.querySelector("html");
htmlElement?.setAttribute("lang", locale);
}
// Public interface
export default {
// Expose the VueI18n instance via a getter
get vueI18n() {
return _i18n;
},
setup,
setLocale,
};

View File

@@ -0,0 +1,18 @@
export default {
en: {
createCalendar: "Create Calendar",
editCalendar: "Edit Calendar",
roomFinder: "Room Finder",
faq: "FAQ",
imprint: "Imprint",
privacy: "Privacy Policy",
},
de: {
createCalendar: "Kalender erstellen",
editCalendar: "Kalender bearbeiten",
roomFinder: "Raumfinder",
faq: "FAQ",
imprint: "Impressum",
privacy: "Datenschutz",
},
};

View File

@@ -0,0 +1,184 @@
{
"languageCode": "de",
"createCalendar": "Kalender erstellen",
"editCalendar": "Kalender bearbeiten",
"roomFinder": "Raumfinder",
"faq": "FAQ",
"imprint": "Impressum",
"privacy": "Datenschutz",
"english": "Englisch",
"german": "Deutsch",
"courseSelection": {
"headline": "Willkommen beim HTWKalender",
"winterSemester": "Wintersemester",
"summerSemester": "Sommersemester",
"subTitle": "Bitte wähle eine Gruppe und das Semester aus.",
"courseDropDown": "Gruppe",
"noCoursesAvailable": "Keine Gruppen verfügbar",
"semesterDropDown": "Semester"
},
"roomFinderPage": {
"headline": "Raumfinder",
"detail": "Bitte wähle einen Raum aus, um die Belegung einzusehen",
"dropDownSelect": "Bitte wähle einen Raum aus",
"noRoomsAvailable": "Keine Räume verfügbar"
},
"moduleSelection": {
"nextStep": "Weiter",
"selectAll": "Alle anwählen",
"deselectAll": "Alle abwählen",
"selected": "angewählt",
"unselected": "abgewählt",
"noModulesAvailable": "Keine Module verfügbar",
"modules": "Module"
},
"moduleInformation": {
"course": "Kurs",
"person": "Dozent",
"semester": "Semester",
"module": "Modul",
"day": "Tag",
"start": "Begin",
"end": "Ende",
"room": "Raum",
"type": "Art",
"week": "Woche"
},
"editCalendarView": {
"error": "Fehler",
"invalidToken": "Ungültiger Token",
"headline": "Bearbeite deinen HTWKalender",
"subTitle": "Füge deinen Link oder Token ein um den Kalender zu bearbeiten",
"loadCalendar": "Kalender laden"
},
"additionalModules": {
"subTitle": "Füge weitere Module hinzu die nicht in deinem Studiengang enthalten sind.",
"dropDown": "Wähle weitere Module aus",
"module": "Modul",
"modules": "Module",
"dropDownFooterSelected": "ausgewählt",
"nextStep": "Weiter"
},
"renameModules": {
"reminder": "Erinnerung",
"enableAllNotifications": "Alle Benachrichtigungen aktivieren",
"subTitle": "Konfigurieren Sie die ausgewählten Module nach Ihren Wünschen.",
"nextStep": "Weiter"
},
"moduleTemplateDialog": {
"explanationOne": "Hier können Module nach Wunsch umbenannt werden, welche dann als Anzeigename im Kalender dargestellt werden.",
"explanationTwo": "Zusätzlich können Sie Benachrichtigungen für jedes Modul einschalten, damit für jeden Termin 15 Minuten vor Beginn einen Erinnerung geschalten wird.",
"tableDescription": "Im Modulnamen stehen folgende Platzhalter zur Verfügung:",
"placeholder": "Platzhalter",
"description": "Beschreibung",
"examples": "Beispiele",
"moduleConfiguration": "Modulkonfiguration",
"mandatory": "Verpflichtend",
"optional": "Optional",
"lecture": "Vorlesung",
"seminar": "Seminar",
"exam": "Prüfung/Praktikum",
"eventTyp": "Ereignistyp"
},
"calendarLink": {
"copyToastNotification": "Link in Zwischenablage kopiert",
"copyToastSummary": "Information",
"copyToastError": "Fehler",
"copyToastErrorDetail": "Link konnte nicht in Zwischenablage kopiert werden",
"copyToClipboard": "Link kopieren",
"toGoogleCalendar": "Google Kalender",
"toMicrosoftCalendar": "Microsoft Kalender"
},
"faqView": {
"headline": "Fragen und Antworten",
"firstQuestion": "Wie funktioniert das Kalender erstellen mit dem HTWKalender?",
"firstAnswer": "Die Webseite ermöglicht es deinen HTWK-Stundenplan in eines deiner bevorzugten Kalender-Verwaltungs-Programme (Outlook, Google Kalender, etc.) einzubinden. ",
"secondQuestion": "Wie genau funktioniert das alles?",
"secondAnswer": "Du wählst deinen Studiengang und das gewünschte Semester aus, danach kannst du aus den dazugehörigen Modulen wählen. Dabei kannst du einzelne Module an/abwählen wie \"Feiertage\" oder Wahlpflichtmodule, die du nicht belegst. Im letzten Schritt wird dir der Link mit dem entsprechenden Token für den von dir erstellenten Kalenders angezeigt. Mit diesem Link kannst du den Kalender abonnieren oder herunterladen. ",
"thirdQuestion": "Wie kann ich den Kalender abonnieren?",
"thirdAnswer": {
"tabTitle": "Google Calendar",
"google": {
"first": "Erstelle deinen Kalender und kopiere den Link.",
"second": "Bei Google Kalender selbst hast du in der linken Seitenleiste eine Sektion namens \"Weitere Kalender\". Dort klickst du das kleine Pfeil-Icon rechts neben dem Schritzug. Im daraufhin erscheinenden Menü gibt es einen Punkt \"Über URL hinzufügen\", den du anklicken musst. ",
"third": "Füge den kopierten Kalenderlink ein, klicke auf \"Kalender hinzufügen\" und du bist fertig. "
},
"microsoft_outlook": {
"title": "Mircosoft Outlook",
"outlook_2010": {
"title": "Unter Outlook 2010:",
"first": "Erstelle deinen Kalender und kopiere den Link.",
"second": "Klicke auf den Reiter “Start”.",
"third": "Dort befindest sich in der Sektion “Kalender verwalten” der Button “Kalender öffnen”, auf den du kicken musst.",
"fourth": "Es öffnet sich ein Kontextmenü, in dem du den Punkt “Aus dem Internet” anklickst.",
"fifth": "Im daraufhin erscheinenden Fenster kannst du den Link einfügen und mit “OK” bestätigen. Eventuell musst du das Abonnement in einem weiteren Schritt noch bestätigen. Dann bist du fertig."
},
"outlook_2007": {
"title": "Unter Outlook 2007:",
"first": "Erstelle deinen Kalender und kopiere den Link.",
"second": "Unter “Extras” findest du die “Kontoeinstellungen”.",
"third": "Dort auf den Reiter “Internetkalender” klicken.",
"fourth": "Auf “Neu” klicken.",
"fifth": "Im sich öffnenden Fenster musst du den Link einfügen. Allerdings musst du “http://” durch “webcal://” austauschen.",
"sixth": "Spätestens beim nächsten Start sollte der Kalender aktualisiert werden und dir fortan zur Verfügung stehen."
}
},
"apple_osx": {
"title": "Calendar (OS X)",
"first": "Erstelle deinen Kalender und kopiere den Link.",
"second": "Unter “Ablage” findest du den Punkt “Neues Kalenderabonnement” (oder unter Snow Leopard “Abonnieren”).",
"third": "Im daraufhin erscheinenden Fenster musst du den kopierten Link einfügen und “abonnieren” klicken.",
"fourth": "Anschließend kannst du dem Kalender noch einen Namen geben und bestimmen, wie oft er aktualisiert werden soll. Falls du iCloud auf deinem iPhone o.ä. verwendest, empfehle ich dir bei “Ort” unbedingt “iCloud” zu wählen. So hast du deinen Stundenplan ohne weiteres zutun auch unterwegs immer parat."
},
"thunderbird": {
"title": "Thunderbird",
"one" : "Erstelle deinen Kalender und kopiere den Link.",
"two" : "Im Menü “Termine und Aufgaben” den Punkt “Kalender” wählen. ",
"three" : "Links siehst du die Kalenderübersicht. In diesem Bereich über die rechte Maustaste klicken und im darauf erscheinenden Kontextmenü “Neuer Kalender” anklicken.",
"four" : "Du hast die Wahl zwischen “Auf meinem Computer” und “Im Netzwerk”. Bitte letzteres wählen und “Fortsetzen” klicken.",
"five" : "Im folgenden Fenster lässt du das “Format” wie es ist (“iCalender”).",
"six" : "Unter \"Adresse\" den kopierten Kalenderlink einfügen.",
"seven" : "Anschließend kannst du noch einen Namen vergeben und weitere Einstellungen nach Belieben vornehmen."
},
"iphone": {
"title": "iPhone",
"description": "Der einfachste Weg unter iOS ist der iCloud-Sync (siehe Anleitung für OS X Kalender). Hier ist der andere Weg: ",
"one": "Erstelle deinen Kalender und kopiere den Link.",
"two": "Gehe in die “Systemeinstellungen”.",
"three": "Dort wählst du “Mail, Kontakte, Kalender”.",
"four": "“Account hinzufügen” auswählen.",
"five": "Ganz unten tippst du auf “Andere”.",
"six": "Der letzte Punkt ist “Kalenderabo hinzufügen”. Dort drauftippen.",
"seven": "Im daraufhin erscheinenden Textfeld fügst du den Kalenderlink ein und drückst oben auf “weiter”.",
"eight": "Du kannst noch eine “Beschreibung” vergeben. Den Rest solltest du lassen, wie er ist.",
"nine": "Nach kurzer Zeit taucht der abonnierte Kalender in der Kalender-App auf."
},
"android": {
"description": "Unter Android ist die Synchronisierung mit dem Google Kalender die einfachste Variante. Schaue bitte in die Google Kalender Anleitung um zu erfahren, wie du den Kalender dort abonnierst. "
},
"windows_phone": {
"description": "Am einfachsten ist unter Windows Phone die Synchronisierung über Outlook.com:",
"one": "Erstelle deinen Kalender und kopiere den Link.",
"two": "Bei Outlook.com anmelden und in den Kalender wechseln.",
"three": "Auf “Subscribe” klicken.",
"four": "“Subscribe to a public calendar” auswählen.",
"five": "Bei “Calendar URL” den Kalenderlink einfügen.",
"six": "Sonstige Einstellungen nach Belieben vornehmen.",
"seven": "Auf “Subscribe to calendar” klicken.",
"eight": "Das Windows-Phone-Gerät muss mit dem gleichen Outlook.com-Benutzerkonto angemeldet sein. Fortan sollte die Synchronisierung des Kalenders automatisch erfolgen."
}
},
"fourthQuestion": "Kalender abonnieren? Ich will den downloaden!",
"fourthAnswer": "Das kannst du gern tun. Nachdem dein persönlicher Stundenplan erstellt wurde, hast du die Möglichkeit ihn herunterzuladen. Außerdem kannst du ihn jederzeit herunterladen, wenn du den generierten Link einfach in deinem Browser aufrufst. Bedenke hierbei, dass heruntergeladene Kalender bzw. Stundenpläne sich nicht aktualisieren werden. Das ist nur möglich, wenn du den Kalender abonnierst. ",
"fifthQuestion": "Ich belege zusätzlich Module aus anderen Studiengängen und möchte diese auch in meinem Stundenplan haben. ",
"fifthAnswer": "Nachdem du die Möglichkeit hattest, die für deinen Studiengang vorgesehenen Module aus dem Modulhandbuch auszuwählen, wirst du auf eine zweite Seite weitergeleitet. Dort hast du die Möglichkeit, weitere Module aus anderen Studiengängen in deinen Studienplan einzufügen. ",
"sixthQuestion": "Aktualisierungs Probleme mit dem Kalender?",
"sixthAnswer": " Das liegt vermutlich daran, dass du ihn heruntergeladen statt abonniert hast. Automatisch aktualisieren können sich nur Kalender, die du abonniert hast. Eine Aktualisierung des Kalenders auf dem Server wird alle 3h ab Mitternacht durchgeführt. So wird gewährleistet das alle Veränderungen seitens der HTWK übernommen werden.",
"seventhQuestion": "Wie lange ist mein Stundenplan bzw. der Link dorthin gültig?",
"seventhAnswer": "Studenpläne sind erstmal nur für die ausgewählten Semester gültig. Da durch Wahlpflichtmodule oder deine Planung sich Veränderungen ergeben können.",
"eighthQuestion": "Preis und Entwicklung?",
"eighthAnswer": "Die Kosten können durch das selbständiges Hosting vollständig ausgelagert werden. Die Entwicklung soll als aktives Git Projekt auch durch die Community verwaltet werden.",
"notFound": "Nicht gefunden, wonach du suchst?",
"contact": "Kontakt aufnehmen"
}
}

View File

@@ -0,0 +1,184 @@
{
"languageCode": "en",
"createCalendar": "create calendar",
"editCalendar": "edit calendar",
"roomFinder": "room finder",
"faq": "faq",
"imprint": "imprint",
"privacy": "privacy",
"english": "English",
"german": "German",
"courseSelection": {
"headline": "Welcome to HTWKalender",
"winterSemester": "winter semester",
"summerSemester": "summer semester",
"subTitle": "Please select a course and semester",
"courseDropDown": "Please select a course",
"noCoursesAvailable": "No courses listed",
"semesterDropDown": "Please select a semester"
},
"roomFinderPage": {
"headline": "room finder",
"detail": "Please select a room to view the occupancy",
"dropDownSelect": "Please select a room",
"noRoomsAvailable": "No rooms listed"
},
"moduleSelection": {
"nextStep": "next step",
"selectAll": "select all",
"deselectAll": "deselect all",
"selected": "selected",
"unselected": "unselected",
"noModulesAvailable": "no modules available",
"modules": "modules"
},
"moduleInformation": {
"course": "course",
"person": "lecturer",
"semester": "semester",
"module": "module",
"day": "day",
"start": "start",
"end": "end",
"room": "room",
"type": "type",
"week": "week"
},
"editCalendarView": {
"error": "error",
"invalidToken": "invalid token",
"headline": "edit your HTWKalender",
"subTitle": "please enter your link or calendar token",
"loadCalendar": "load calendar"
},
"additionalModules": {
"subTitle": "Select additional Modules that are not listed in the regular semester for your Course",
"dropDown": "Select additional modules",
"module": "module",
"modules": "modules",
"dropDownFooterSelected": "selected",
"nextStep": "next step"
},
"renameModules": {
"reminder": "reminder",
"enableAllNotifications": "enable all notifications",
"subTitle": "Configure your selected Modules to your liking.",
"nextStep": "next step"
},
"moduleTemplateDialog": {
"explanationOne": "Here you can rename your modules to your liking. This will be the name of the event in your calendar.",
"explanationTwo": "Additionally, you can toggle notifications for each module. If you do so, you will be notified 15 minutes before the event starts.",
"tableDescription": "You can use the following placeholders in your module names:",
"placeholder": "placeholder",
"description": "description",
"examples": "examples",
"moduleConfiguration": "module configuration",
"mandatory": "mandatory",
"optional": "optional",
"lecture": "lecture",
"seminar": "seminar",
"exam": "exam/internship project",
"eventTyp": "event type"
},
"calendarLink": {
"copyToastNotification": "Link copied to clipboard",
"copyToastSummary": "information",
"copyToastError": "error",
"copyToastErrorDetail": "could not copy link to clipboard",
"copyToClipboard": "copy to clipboard",
"toGoogleCalendar": "to Google Calendar",
"toMicrosoftCalendar": "to Microsoft Calendar"
},
"faqView": {
"headline": "faq",
"firstQuestion": "How does calendar creation work with HTWKalender?",
"firstAnswer": "The website allows you to integrate your HTWK timetable into one of your preferred calendar management programs (Outlook, Google Calendar, etc.).",
"secondQuestion": "How does it all work exactly?",
"secondAnswer": "You choose your course of study and the desired semester; then, you can select modules associated with it. You can individually select or deselect modules such as 'Holidays' or elective modules you are not taking. In the last step, the link with the corresponding token for your created calendar is displayed. With this link, you can subscribe to or download the calendar.",
"thirdQuestion": "How can I subscribe to the calendar?",
"thirdAnswer": {
"tabTitle": "Google Calendar",
"google": {
"first": "Create your calendar and copy the link.",
"second": "In Google Calendar itself, in the left sidebar, there is a section called 'Other calendars.' There, click the small arrow icon to the right of the text. In the menu that appears, there is an option 'Add by URL' that you need to click.",
"third": "Paste the copied calendar link, click 'Add Calendar,' and you're done."
},
"microsoft_outlook": {
"title": "Microsoft Outlook",
"outlook_2010": {
"title": "Under Outlook 2010:",
"first": "Create your calendar and copy the link.",
"second": "Click on the 'Home' tab.",
"third": "In the 'Manage Calendars' section, there is a 'Open Calendar' button that you need to click.",
"fourth": "A context menu opens, where you click on 'From Internet'.",
"fifth": "In the window that appears, you can paste the link and confirm with 'OK'. You may need to confirm the subscription in an additional step. Then you're done."
},
"outlook_2007": {
"title": "Under Outlook 2007:",
"first": "Create your calendar and copy the link.",
"second": "Under 'Tools', you will find 'Account Settings'.",
"third": "There, click on the 'Internet Calendars' tab.",
"fourth": "Click on 'New.'",
"fifth": "In the opening window, you must paste the link. However, you need to replace 'http://' with 'webcal://'.",
"sixth": "At the latest, the calendar should be updated on the next start and be available to you from then on."
}
},
"apple_osx": {
"title": "Calendar (OS X)",
"first": "Create your calendar and copy the link.",
"second": "Under 'File', you will find the option 'New Calendar Subscription' (or 'Subscribe' under Snow Leopard).",
"third": "In the window that appears, paste the copied link and click 'Subscribe.'",
"fourth": "You can then give the calendar a name and determine how often it should be updated. If you use iCloud on your iPhone or similar, I recommend selecting 'iCloud' under 'Location.' This way, your timetable is always available on the go without further action."
},
"thunderbird": {
"title": "Thunderbird",
"one": "Create your calendar and copy the link.",
"two": "In the 'Events and Tasks' menu, select 'Calendar.'",
"three": "On the left, you will see the calendar overview. Click with the right mouse button in this area, and in the context menu that appears, click 'New Calendar.'",
"four": "You have the choice between 'On My Computer' and 'On the Network.' Please choose the latter and click 'Continue.'",
"five": "In the following window, leave the 'Format' as it is ('iCalendar').",
"six": "Paste the copied calendar link under 'Location'.",
"seven": "You can then give it a name and make additional settings as desired."
},
"iphone": {
"title": "iPhone",
"description": "The easiest way on iOS is through iCloud sync (see instructions for OS X Calendar). Here is another way:",
"one": "Create your calendar and copy the link.",
"two": "Go to 'Settings'.",
"three": "There, select 'Mail, Contacts, Calendars'.",
"four": "Select 'Add Account.'",
"five": "At the bottom, tap 'Other.'",
"six": "The last option is 'Add Subscribed Calendar.' Tap on it.",
"seven": "In the text field that appears, paste the calendar link and press 'Next' at the top.",
"eight": "You can give it a 'Description.' Leave the rest as it is.",
"nine": "After a short time, the subscribed calendar will appear in the Calendar app."
},
"android": {
"description": "Under Android, synchronizing with Google Calendar is the easiest option. Please refer to the Google Calendar instructions to learn how to subscribe to the calendar there."
},
"windows_phone": {
"description": "The easiest way on Windows Phone is synchronization via Outlook.com:",
"one": "Create your calendar and copy the link.",
"two": "Log in to Outlook.com and switch to the calendar.",
"three": "Click on 'Subscribe.'",
"four": "Select 'Subscribe to a public calendar.'",
"five": "Paste the calendar link under 'Calendar URL.'",
"six": "Make other settings as desired.",
"seven": "Click on 'Subscribe to calendar.'",
"eight": "The Windows Phone device must be logged in with the same Outlook.com user account. From now on, calendar synchronization should occur automatically."
}
},
"fourthQuestion": "Subscribe to the calendar? I want to download it!",
"fourthAnswer": "You can certainly do that. After your personal timetable has been created, you have the option to download it. Additionally, you can download it anytime by simply opening the generated link in your browser. Keep in mind that downloaded calendars or timetables will not update. This is only possible if you subscribe to the calendar.",
"fifthQuestion": "I am taking additional modules from other courses of study and want them in my timetable.",
"fifthAnswer": "After you have had the opportunity to select the modules provided for your course of study from the module handbook, you will be redirected to a second page. There, you have the option to add additional modules from other courses of study to your study plan.",
"sixthQuestion": "Updating issues with the calendar?",
"sixthAnswer": "This is probably because you downloaded it instead of subscribing. Only calendars that you have subscribed to can update automatically. A server update of the calendar occurs every 3 hours starting from midnight. This ensures that all changes from HTWK are incorporated.",
"seventhQuestion": "How long is my timetable or the link to it valid?",
"seventhAnswer": "Timetables are initially only valid for the selected semesters, as changes can occur due to elective modules or your planning.",
"eighthQuestion": "Cost and development?",
"eighthAnswer": "Costs can be completely outsourced through self-hosting. Development is intended to be managed by the community as an active Git project.",
"notFound": "Not finding what you're looking for?",
"contact": "Get in touch"
}
}

View File

@@ -4,6 +4,7 @@ import App from "./App.vue";
import PrimeVue from "primevue/config";
import Button from "primevue/button";
import Dropdown from "primevue/dropdown";
import Menu from "primevue/menu";
import Menubar from "primevue/menubar";
import InputText from "primevue/inputtext";
import InputSwitch from "primevue/inputswitch";
@@ -30,6 +31,7 @@ import Column from "primevue/column";
import DynamicDialog from "primevue/dynamicdialog";
import DialogService from "primevue/dialogservice";
import ProgressSpinner from "primevue/progressspinner";
import i18n from "./i18n";
const app = createApp(App);
const pinia = createPinia();
@@ -39,7 +41,10 @@ app.use(router);
app.use(ToastService);
app.use(pinia);
app.use(DialogService);
i18n.setup();
app.use(i18n.vueI18n);
app.component("Button", Button);
app.component("Menu", Menu);
app.component("Menubar", Menubar);
app.component("Dialog", Dialog);
app.component("Dropdown", Dropdown);

View File

@@ -1,15 +1,16 @@
import { createRouter, createWebHistory } from "vue-router";
import Faq from "../components/FaqPage.vue";
import CourseSelection from "../components/CourseSelection.vue";
import AdditionalModules from "../components/AdditionalModules.vue";
import AdditionalModules from "../view/AdditionalModules.vue";
import CalendarLink from "../components/CalendarLink.vue";
import Imprint from "../components/ImprintPage.vue";
import PrivacyPolicy from "../components/PrivacyPolicy.vue";
import Imprint from "../view/ImprintPage.vue";
import PrivacyPolicy from "../view/PrivacyPolicy.vue";
import RenameModules from "../components/RenameModules.vue";
import RoomFinder from "../components/RoomFinder.vue";
import EditCalendarView from "../view/editCalendarView.vue";
import RoomFinder from "../view/RoomFinder.vue";
import EditCalendarView from "../view/EditCalendarView.vue";
import EditAdditionalModules from "../components/editCalendar/EditAdditionalModules.vue";
import EditModules from "../components/editCalendar/EditModules.vue";
import CourseSelection from "../components/CourseSelection.vue";
import i18n from "../i18n";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -69,11 +70,17 @@ const router = createRouter({
name: "rename-modules",
component: RenameModules,
},
{
path: "/:catchAll(.*)",
redirect: "/",
},
],
});
router.beforeEach(async (to, from) => {
const newLocale = to.params.locale;
const prevLocale = from.params.locale;
// If the locale hasn't changed, do nothing
if (newLocale === prevLocale) {
return;
}
i18n.setLocale(newLocale);
});
export default router;

View File

@@ -0,0 +1,17 @@
import { defineStore } from "pinia";
import { useLocalStorage } from "@vueuse/core";
const localeStore = defineStore("localeStore", {
state: () => {
return {
locale: useLocalStorage("locale", "en"), //useLocalStorage takes in a key of 'count' and default value of 0
};
},
actions: {
setLocale(locale: string) {
this.locale = locale;
},
},
});
export default localeStore;

View File

@@ -1,15 +1,18 @@
<script lang="ts" setup>
import { defineAsyncComponent, ref, Ref } from "vue";
import { Module } from "../model/module.ts";
import { fetchAllModules } from "../api/fetchCourse.ts";
import {defineAsyncComponent, ref, Ref} from "vue";
import {Module} from "../model/module.ts";
import {fetchAllModules} from "../api/fetchCourse.ts";
import moduleStore from "../store/moduleStore.ts";
import {MultiSelectAllChangeEvent} from "primevue/multiselect";
import { FilterMatchMode } from "primevue/api";
import { useDialog } from "primevue/usedialog";
const dialog = useDialog();
import router from "../router";
import { fetchModule } from "../api/fetchModule.ts";
import {fetchModule} from "../api/fetchModule.ts";
import {useDialog} from "primevue/usedialog";
import {useI18n} from "vue-i18n";
const dialog = useDialog();
const { t } = useI18n({ useScope: "global" });
const fetchedModules = async () => {
return await fetchAllModules();
@@ -51,7 +54,7 @@ async function nextStep() {
}
const ModuleInformation = defineAsyncComponent(
() => import("./ModuleInformation.vue"),
() => import("../components/ModuleInformation.vue"),
);
async function showInfo(module: Module) {
@@ -110,6 +113,14 @@ function selectChange(event : MultiSelectChangeEvent) {
}
selectAll.value = store.countModules() === modules.value.length;
}
function itemsLabel(selectedModules: Module[]): string {
return (selectedModules ? selectedModules.length : 0) != 1 ? t("additionalModules.modules") : t("additionalModules.module");
}
function itemsLabelWithNumber(selectedModules: Module[]): string {
return selectedModules.length.toString() + " " + itemsLabel(selectedModules) + " " + t("additionalModules.dropDownFooterSelected");
}
*/
</script>
@@ -117,8 +128,7 @@ function selectChange(event : MultiSelectChangeEvent) {
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3>
Select additional Modules that are not listed in the regular semester
for your Course
{{ $t("additionalModules.subTitle") }}
</h3>
</div>
<div class="card flex align-items-center justify-content-center m-2">
@@ -139,7 +149,7 @@ function selectChange(event : MultiSelectChangeEvent) {
:striped-rows="true"
class="w-10"
>
<Column
selection-mode="multiple"
>
@@ -202,7 +212,7 @@ function selectChange(event : MultiSelectChangeEvent) {
@click.stop="showInfo(slotProps.data)"
></Button>
</template>
</Column>
<template #footer>
<div class="py-2 px-3">
@@ -226,9 +236,14 @@ function selectChange(event : MultiSelectChangeEvent) {
:virtual-scroller-options="{ itemSize: 70 }"
class="custom-multiselect"
filter
:placeholder="$t('additionalModules.dropDown')"
:auto-filter-focus="true"
:show-toggle-all="false"
@change="selectChange()"
placeholder="Select additional modules"
@change="selectChange($event)"
@selectall-change="onSelectAllChange($event)"
:selectedItemsLabel="itemsLabelWithNumber(selectedModules)"
>
<template #option="slotProps">
<div class="flex justify-content-between w-full">
@@ -254,17 +269,17 @@ function selectChange(event : MultiSelectChangeEvent) {
<template #footer>
<div class="py-2 px-3">
<b>{{ selectedModules ? selectedModules.length : 0 }}</b>
item{{
(selectedModules ? selectedModules.length : 0) > 1 ? "s" : ""
}}
selected.
{{ itemsLabel(selectedModules) }}
{{ $t("additionalModules.dropDownFooterSelected") }}
</div>
</template>
</MultiSelect>
-->
</div>
<div class="flex align-items-center justify-content-center h-4rem m-2">
<Button @click="nextStep()">Next Step</Button>
<Button @click="nextStep()">{{
$t("additionalModules.nextStep")
}}</Button>
</div>
</div>
</template>

View File

@@ -6,6 +6,9 @@ import { getCalender } from "../api/loadCalendar";
import router from "../router";
import tokenStore from "../store/tokenStore";
import { useToast } from "primevue/usetoast";
import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" });
const toast = useToast();
const token: Ref<string> = ref("");
@@ -13,7 +16,7 @@ const modules: Ref<Module[]> = ref(moduleStore().modules);
function extractToken(token: string): string {
const tokenRegex = /^[a-z0-9]{15}$/;
const tokenUriRegex = /(?:\?|&)token=([a-z0-9]{15})(?:&|$)/;
const tokenUriRegex = /[?&]token=([a-z0-9]{15})(?:&|$)/;
if (tokenRegex.test(token)) {
return token;
@@ -33,8 +36,8 @@ function loadCalendar(): void {
} catch (e) {
toast.add({
severity: "error",
summary: "Error",
detail: "Invalid token",
summary: t("editCalendarView.error"),
detail: t("editCalendarView.invalidToken"),
life: 3000,
});
return;
@@ -58,10 +61,10 @@ function loadCalendar(): void {
<Toast />
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem mt-2">
<h3 class="text-2xl">
Edit your HTWKalender
<h3 class="text-4xl">
{{ $t("editCalendarView.headline") }}
<i
class="pi pi-calendar vertical-align-baseline"
class="pi pi-pencil vertical-align-baseline ml-2"
style="font-size: 2rem"
></i>
</h3>
@@ -69,17 +72,17 @@ function loadCalendar(): void {
<div
class="flex align-items-center justify-content-center h-4rem border-round"
>
<p class="text-2xl">Please enter your existing calendar token</p>
<p class="text-2xl">{{ $t("editCalendarView.subTitle") }}</p>
</div>
<div
class="flex align-items-center justify-content-center border-round m-2"
>
<InputText v-model="token" type="text" />
<InputText v-model="token" type="text" autofocus @keyup.enter="loadCalendar" />
</div>
<div
class="flex align-items-center justify-content-center border-round m-2"
>
<Button label="Load Calendar" @click="loadCalendar" />
<Button :label="$t('editCalendarView.loadCalendar')" @click="loadCalendar" />
</div>
</div>
</template>

View File

@@ -3,14 +3,15 @@
<template>
<div class="flex align-items-center justify-content-center flex-column">
<div class="flex align-items-center justify-content-center h-4rem mt-2">
<h3 class="text-4xl">Imprint</h3>
<h3 class="text-4xl">{{$t("imprint")}}</h3>
</div>
<div class="flex flex-column col-7">
<p>nach dem Telemediengesetz (TMG) der Bundesrepublik Deutschland.</p>
<h2>Kontakt</h2>
<p>
Per Email: <a href="mailto:support@ekresse.de">support@ekresse.de</a>
Per Email:
<a href="mailto:support@htwkalender.de">support@htwkalender.de</a>
</p>
<h2>Adresse</h2>

View File

@@ -3,7 +3,7 @@
<template>
<div class="flex align-items-center justify-content-center flex-column">
<div class="flex align-items-center justify-content-center h-4rem mt-2">
<h3 class="text-4xl">Privacy policy</h3>
<h3 class="text-4xl">{{$t("privacy")}}</h3>
</div>
<div class="flex flex-column col-7">
<h1>Datenschutzerklärung</h1>
@@ -55,7 +55,7 @@
Deutschland
</p>
E-Mail-Adresse:
<p><a href="mailto:support@ekresse.de">support@ekresse.de</a></p>
<p><a href="mailto:support@htwkalender.de">support@htwkalender.de</a></p>
<h2 id="mOverview">Übersicht der Verarbeitungen</h2>
<p>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { Ref, ref } from "vue";
import { fetchRoom } from "../api/fetchRoom.ts";
import RoomOccupation from "./RoomOccupation.vue";
import RoomOccupation from "../components/RoomOccupation.vue";
const rooms = async () => {
return await fetchRoom();
@@ -21,12 +21,16 @@ rooms().then(
<template>
<div class="flex flex-column">
<div class="flex align-items-center justify-content-center h-4rem m-2">
<h3 class="text-4xl">Raumfinder</h3>
<h3 class="text-4xl">{{ $t("roomFinderPage.headline") }}</h3>
<i
class="pi pi-search vertical-align-baseline ml-3"
style="font-size: 2rem"
></i>
</div>
<div
class="flex align-items-center justify-content-center h-4rem border-round m-2"
>
<h5 class="text-2xl">Please select a room</h5>
<h5 class="text-2xl">{{ $t("roomFinderPage.detail") }}</h5>
</div>
<div
class="flex align-items-center justify-content-center border-round m-2"
@@ -37,7 +41,9 @@ rooms().then(
class="w-full md:w-25rem mx-2"
filter
option-label="name"
placeholder="Select a Room"
:placeholder="$t('roomFinderPage.dropDownSelect')"
:empty-message="$t('roomFinderPage.noRoomsAvailable')"
:auto-filter-focus="true"
/>
</div>
<div class="m-6">