Merge remote-tracking branch 'origin' into 3-semi-offline-room-finder

This commit is contained in:
survellow
2024-05-29 23:19:48 +02:00
31 changed files with 1745 additions and 1541 deletions

View File

@ -14,8 +14,6 @@
#You should have received a copy of the GNU Affero General Public License #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/>. #along with this program. If not, see <https://www.gnu.org/licenses/>.
version: "3.9"
services: services:
htwkalender-backend: htwkalender-backend:
build: build:

View File

@ -11,7 +11,7 @@
<link rel="mask-icon" href="/htwk-mask.svg" color="#00494c" /> <link rel="mask-icon" href="/htwk-mask.svg" color="#00494c" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="theme-color" content="#1b2022"> <meta name="theme-color" content="#1b2022">
<link <link
id="theme-link" id="theme-link"
rel="stylesheet" rel="stylesheet"
href="/themes/lara-light-blue/theme.css" href="/themes/lara-light-blue/theme.css"

File diff suppressed because it is too large Load Diff

View File

@ -13,42 +13,46 @@
"test": "vitest" "test": "vitest"
}, },
"dependencies": { "dependencies": {
"@fullcalendar/core": "^6.1.11", "@fullcalendar/core": "^6.1.13",
"@fullcalendar/daygrid": "^6.1.11", "@fullcalendar/daygrid": "^6.1.13",
"@fullcalendar/interaction": "^6.1.11", "@fullcalendar/icalendar": "^6.1.13",
"@fullcalendar/timegrid": "^6.1.11", "@fullcalendar/interaction": "^6.1.13",
"@fullcalendar/vue3": "^6.1.11", "@fullcalendar/timegrid": "^6.1.13",
"@tanstack/vue-query": "^5.28.9", "@fullcalendar/vue3": "^6.1.13",
"@tanstack/vue-query-devtools": "^5.28.10", "@tanstack/vue-query": "^5.37.1",
"@tanstack/vue-query-devtools": "^5.37.1",
"@types/ical": "^0.8.3",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"bson": "^5.5.1", "bson": "^5.5.1",
"country-flag-emoji-polyfill": "^0.1.8", "country-flag-emoji-polyfill": "^0.1.8",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"date-fns-tz": "^3.1.3", "date-fns-tz": "^3.1.3",
"ical.js": "^1.5.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"primeflex": "^3.3.1", "primeflex": "^3.3.1",
"primeicons": "^6.0.1", "primeicons": "^6.0.1",
"primevue": "^3.50.0", "primevue": "^3.52.0",
"source-sans": "^3.46.0", "source-sans": "^3.46.0",
"vue": "^3.4.11", "vue": "^3.4.11",
"vue-i18n": "^9.10.2", "vue-i18n": "^9.13.1",
"vue-router": "^4.3.0" "vue-router": "^4.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.12.2", "@types/node": "^20.12.12",
"@vitejs/plugin-basic-ssl": "^1.1.0",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.24.0", "eslint-plugin-vue": "^9.26.0",
"prettier": "3.2.1", "prettier": "3.2.1",
"sass": "^1.72.0", "sass": "^1.77.2",
"sass-loader": "^13.3.3", "sass-loader": "^13.3.3",
"typescript": "^5.4.3", "typescript": "^5.4.5",
"vite": "^5.2.7", "vite": "^5.2.11",
"vite-plugin-mkcert": "^1.17.5", "vite-plugin-pwa": "^0.20.0",
"vite-plugin-pwa": "^0.19.8", "vitest": "^1.6.0",
"vitest": "^1.4.0",
"vue-tsc": "^1.8.27" "vue-tsc": "^1.8.27"
} }
} }

View File

@ -7,5 +7,5 @@ npx sass public/primevue-sass-theme/themes/lara/lara-dark/yellow/theme.scss publ
``` ```
```bash ```bash
npx sass public/primevue-sass-theme/themes/lara/lara-light/blue/theme.scss public/themes/lara-light-blue/theme.css npx sass public/primevue-sass-theme/themes/lara/lara-light/green/theme.scss public/themes/lara-light-blue/theme.css
``` ```

View File

@ -53,6 +53,18 @@ $highlightFocusBg: rgba($primaryColor, 0.24) !default;
color: map-get($colors, "htwk-schwarz"); /*#1c2127*/ color: map-get($colors, "htwk-schwarz"); /*#1c2127*/
} }
.fc-v-event {
background: map-get($colors, "primary");
border: 2px solid rgba(193, 180, 0, 0.94);
color: map-get($colors, "htwk-schwarz"); /*#1c2127*/
border-radius: 6px;
margin: 1%;
}
.fc-v-event .fc-event-main {
color: map-get($colors, "htwk-schwarz"); /*#1c2127*/
}
.fc.fc-unthemed .fc-view-container .fc-divider { .fc.fc-unthemed .fc-view-container .fc-divider {
background: map-get($colors, "htwk-grau"); /*#071426*/ background: map-get($colors, "htwk-grau"); /*#071426*/
border: 1px solid map-get($colors, "htwk-grau-140"); /*#0b213f*/ border: 1px solid map-get($colors, "htwk-grau-140"); /*#0b213f*/

View File

@ -56,6 +56,13 @@ $highlightFocusBg: rgba($primaryColor, .24) !default;
color: $primaryTextColor; /*#1c2127*/ color: $primaryTextColor; /*#1c2127*/
} }
.fc-v-event {
background: map-get($colors, "primary");
border: 2px solid rgba(0, 121, 62, 0.94);
border-radius: 6px;
margin: 1%;
}
.fc.fc-unthemed .fc-view-container .fc-divider { .fc.fc-unthemed .fc-view-container .fc-divider {
background: $primaryTextColor; /*#071426*/ background: $primaryTextColor; /*#071426*/
border: 1px solid map-get($colors, "htwk-grau-140"); /*#0b213f*/ border: 1px solid map-get($colors, "htwk-grau-140"); /*#0b213f*/

View File

@ -10133,6 +10133,16 @@
border: 1px solid rgba(255, 237, 0, 0.24); border: 1px solid rgba(255, 237, 0, 0.24);
color: #000000; /*#1c2127*/ color: #000000; /*#1c2127*/
} }
:root .fc-v-event {
background: #ffed00;
border: 2px solid rgba(193, 180, 0, 0.94);
color: #000000; /*#1c2127*/
border-radius: 6px;
margin: 1%;
}
:root .fc-v-event .fc-event-main {
color: #000000; /*#1c2127*/
}
:root .fc.fc-unthemed .fc-view-container .fc-divider { :root .fc.fc-unthemed .fc-view-container .fc-divider {
background: #2e3639; /*#071426*/ background: #2e3639; /*#071426*/
border: 1px solid #1b2022; /*#0b213f*/ border: 1px solid #1b2022; /*#0b213f*/

File diff suppressed because one or more lines are too long

View File

@ -10132,6 +10132,12 @@
border: 1px solid rgba(0, 148, 76, 0.24); border: 1px solid rgba(0, 148, 76, 0.24);
color: #ffffff; /*#1c2127*/ color: #ffffff; /*#1c2127*/
} }
:root .fc-v-event {
background: #00944c;
border: 2px solid rgba(0, 121, 62, 0.94);
border-radius: 6px;
margin: 1%;
}
:root .fc.fc-unthemed .fc-view-container .fc-divider { :root .fc.fc-unthemed .fc-view-container .fc-divider {
background: #ffffff; /*#071426*/ background: #ffffff; /*#071426*/
border: 1px solid #1b2022; /*#0b213f*/ border: 1px solid #1b2022; /*#0b213f*/

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
//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/>.
export async function fetchICalendarEvents(token: string): Promise<string> {
const response = await fetch("/api/feed?token=" + token, { method: "GET" });
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.text();
}

View File

@ -76,6 +76,14 @@ const forwardToMicrosoft = () => {
); );
}; };
const forwardToHTWKalendar = () => {
// route to path /calendar/view?token=token
router.push({
name: "calendar-view",
query: { token: tokenStore().token },
});
};
const actions = computed(() => [ const actions = computed(() => [
{ {
label: t("calendarLink.copyToClipboard"), label: t("calendarLink.copyToClipboard"),
@ -92,6 +100,11 @@ const actions = computed(() => [
icon: "pi pi-microsoft", icon: "pi pi-microsoft",
command: forwardToMicrosoft, command: forwardToMicrosoft,
}, },
{
label: t("calendarLink.toHTWKalendar"),
icon: "pi pi-home",
command: forwardToHTWKalendar,
}
]); ]);
</script> </script>

View File

@ -0,0 +1,133 @@
<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 allLocales from "@fullcalendar/core/locales-all";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
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 tokenStore from "@/store/tokenStore.ts";
import { parseICalData } from "@/helpers/ical.ts";
import { fetchICalendarEvents } from "@/api/loadICal.ts";
const { t } = useI18n({ useScope: "global" });
const props = defineProps({
token: {
type: String,
required: true,
},
});
const selectedToken = computed(() => props.token);
const mobilePage = inject("mobilePage") as Ref<boolean>;
const date: Ref<Date> = ref(new Date());
const { data: calendar } = useQuery({
queryKey: ["userCalendar", selectedToken],
queryFn: () =>
fetchICalendarEvents(selectedToken.value),
select: (data) => {
return data;
},
networkMode: "offlineFirst",
enabled: () => tokenStore().token !== "",
staleTime: 5000000, // 500 seconds
});
const fullCalendar = ref<InstanceType<typeof FullCalendar>>();
const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
locales: allLocales,
locale: t("languageCode"),
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, iCalenderPlugin],
// local debugging of mobilePage variable in object creation on ternary expression
initialView: mobilePage.value ? "Day" : "week",
initialDate: date.value,
dayHeaderFormat: { weekday: "short", omitCommas: true },
slotDuration: "00:15:00",
eventTimeFormat: {
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
height: "auto",
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",
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],
},
},
headerToolbar: {
end: "prev,next today",
center: "title",
start: "week,Day",
},
datesSet: function (dateInfo: DatesSetArg) {
const view = dateInfo.view;
const offset = new Date().getTimezoneOffset();
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
router.replace({
query: {
...router.currentRoute.value.query,
date: formatYearMonthDay(endDate),
},
});
},
events: parseICalData(calendar.value),
}));
watch(mobilePage, () => {
fullCalendar.value?.getApi().changeView(mobilePage.value ? "Day" : "week");
});
</script>
<template>
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
</template>
<style scoped>
:deep(.fc-toolbar.fc-header-toolbar) {
flex-wrap: wrap;
justify-content: space-between;
gap: 0.5rem;
}
</style>

View File

@ -27,14 +27,26 @@ const isDark = ref(true);
const items = computed(() => [ const items = computed(() => [
{ {
label: t("createCalendar"), label: t("calendar"),
icon: "pi pi-fw pi-plus", icon: "pi pi-fw pi-angle-down",
route: "/", info: "calendar",
}, items: [
{ {
label: t("editCalendar"), label: t("createCalendar"),
icon: "pi pi-fw pi-pencil", icon: "pi pi-fw pi-plus",
route: "/edit", route: "/calendar/create",
},
{
label: t("editCalendar"),
icon: "pi pi-fw pi-pencil",
route: "/calendar/edit",
},
{
label: t("userCalendar"),
icon: "pi pi-fw pi-calendar",
route: "/calendar/view",
},
],
}, },
{ {
label: t("rooms"), label: t("rooms"),

View File

@ -175,9 +175,12 @@ const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
id: event.id.toString(), id: event.id.toString(),
start: event.start, start: event.start,
end: event.end, end: event.end,
color: event.showFree backgroundColor: event.showFree
? "var(--htwk-gruen-500)" ? "var(--htwk-gruen-500)"
: "var(--htwk-grau-60-500)", : "var(--htwk-grau-60-500)",
borderColor: event.showFree
? "var(--htwk-gruen-600)"
: "var(--htwk-grau-60-600)",
textColor: event.showFree textColor: event.showFree
? "var(--green-50)" ? "var(--green-50)"
: "white", : "white",

View File

@ -0,0 +1,24 @@
import ICAL from 'ical.js';
import { CalendarComponent } from 'ical';
export function parseICalData(icalData: string | undefined) {
if (icalData === undefined || !icalData) {
return [];
}
const jCalData = ICAL.parse(icalData);
const comp = new ICAL.Component(jCalData);
const vEvents = comp.getAllSubcomponents('vevent');
return vEvents.map((vevent: CalendarComponent) => {
const event = new ICAL.Event(vevent);
return {
title: event.summary,
start: event.startDate.toJSDate(),
end: event.endDate.toJSDate(),
allDay: event.startDate.isDate,
// Include other properties as needed
};
});
}

View File

@ -0,0 +1,19 @@
const tokenRegex = /^[a-z0-9]{15}$/;
const tokenUriRegex = /[?&]token=([a-z0-9]{15})(?:&|$)/;
export const isToken = (token: string): boolean => {
return tokenRegex.test(token) || tokenUriRegex.test(token);
};
export function extractToken(token: string): string {
if (tokenRegex.test(token)) {
return token;
}
const match = tokenUriRegex.exec(token);
if (match) {
return match[1];
}
throw new Error("Invalid token");
}

View File

@ -2,6 +2,8 @@
"languageCode": "de", "languageCode": "de",
"createCalendar": "Kalender erstellen", "createCalendar": "Kalender erstellen",
"editCalendar": "Kalender bearbeiten", "editCalendar": "Kalender bearbeiten",
"userCalendar": "Dein Kalender",
"calendar": "Kalender",
"rooms": "Räume", "rooms": "Räume",
"faq": "FAQ", "faq": "FAQ",
"imprint": "Impressum", "imprint": "Impressum",
@ -139,7 +141,8 @@
"copyToastErrorDetail": "Link konnte nicht in Zwischenablage kopiert werden", "copyToastErrorDetail": "Link konnte nicht in Zwischenablage kopiert werden",
"copyToClipboard": "Link kopieren", "copyToClipboard": "Link kopieren",
"toGoogleCalendar": "Google Kalender", "toGoogleCalendar": "Google Kalender",
"toMicrosoftCalendar": "Microsoft Kalender" "toMicrosoftCalendar": "Microsoft Kalender",
"toHTWKalendar": "HTWKalender"
}, },
"calendarPreview": { "calendarPreview": {
"preview": "Vorschau", "preview": "Vorschau",
@ -240,5 +243,11 @@
"ninthAnswer": "Wenn du dich für die Entwicklung und den Quelltext interessierst, kannst du jederzeit als HTWK-Student daran mitarbeiten. Quelltext und weitere Informationen findest du im ", "ninthAnswer": "Wenn du dich für die Entwicklung und den Quelltext interessierst, kannst du jederzeit als HTWK-Student daran mitarbeiten. Quelltext und weitere Informationen findest du im ",
"notFound": "Nicht gefunden, wonach du suchst?", "notFound": "Nicht gefunden, wonach du suchst?",
"contact": "Kontakt aufnehmen" "contact": "Kontakt aufnehmen"
},
"userCalender": {
"headline": "Dein Kalender",
"subTitle": "Hier findest du die Kalenderansicht von deinem persönlichen Feed.",
"searchPlaceholder": "Token",
"searchButton": "Kalender laden"
} }
} }

View File

@ -2,6 +2,8 @@
"languageCode": "en", "languageCode": "en",
"createCalendar": "create calendar", "createCalendar": "create calendar",
"editCalendar": "edit calendar", "editCalendar": "edit calendar",
"userCalendar": "user calendar",
"calendar": "calendar",
"rooms": "rooms", "rooms": "rooms",
"faq": "faq", "faq": "faq",
"imprint": "imprint", "imprint": "imprint",
@ -138,8 +140,9 @@
"copyToastError": "error", "copyToastError": "error",
"copyToastErrorDetail": "could not copy link to clipboard", "copyToastErrorDetail": "could not copy link to clipboard",
"copyToClipboard": "copy to clipboard", "copyToClipboard": "copy to clipboard",
"toGoogleCalendar": "to Google Calendar", "toGoogleCalendar": "Google Calendar",
"toMicrosoftCalendar": "to Microsoft Calendar" "toMicrosoftCalendar": "Microsoft Calendar",
"toHTWKalendar": "HTWKalender"
}, },
"calendarPreview": { "calendarPreview": {
"preview": "preview", "preview": "preview",
@ -240,5 +243,11 @@
"ninthAnswer": "If you want to contribute, you can do so at any time if you are a HTWK student. The source code is available on ", "ninthAnswer": "If you want to contribute, you can do so at any time if you are a HTWK student. The source code is available on ",
"notFound": "Not finding what you're looking for?", "notFound": "Not finding what you're looking for?",
"contact": "Get in touch" "contact": "Get in touch"
},
"userCalender": {
"headline": "user calendar",
"subTitle": "Here you can find the calendar view of your personal feed.",
"searchPlaceholder": "calendar token",
"searchButton": "load calendar"
} }
} }

View File

@ -1,245 +1,253 @@
{ {
"languageCode": "ja", "languageCode": "ja",
"createCalendar": "カレンダーを作成", "createCalendar": "カレンダーを作成",
"editCalendar": "カレンダーを編集", "editCalendar": "カレンダーを編集",
"rooms": "部屋", "userCalendar": "ユーザーカレンダー",
"faq": "よくある質問", "calendar": "カレンダー",
"imprint": "インプリント", "rooms": "部屋",
"privacy": "プライバシー", "faq": "よくある質問",
"english": "英語", "imprint": "インプリント",
"german": "ドイツ語", "privacy": "プライバシー",
"japanese": "日本語", "english": "語",
"courseSelection": { "german": "ドイツ語",
"headline": "HTWカレンダーへようこそ", "japanese": "日本語",
"winterSemester": "冬学期", "courseSelection": {
"summerSemester": "夏学期", "headline": "HTWカレンダーへようこそ",
"subTitle": "コースと学期を選択してください", "winterSemester": "冬学期",
"nextStep": "次のステップ", "summerSemester": "夏学期",
"courseDropDown": "コースを選択してください", "subTitle": "コースと学期を選択してください",
"noCoursesAvailable": "利用可能なコースがありません", "nextStep": "次のステップ",
"semesterDropDown": "学期を選択してください" "courseDropDown": "コースを選択してください",
"noCoursesAvailable": "利用可能なコースがありません",
"semesterDropDown": "学期を選択してください"
},
"roomFinderPage": {
"roomSchedule": "部屋の占有状況",
"headline": "部屋の占有計画",
"detail": "占有状況を表示するために部屋を選択してください。",
"dropDownSelect": "部屋を選択してください",
"noRoomsAvailable": "利用可能な部屋がありません",
"available": "利用可能",
"occupied": "占有中",
"stub": "オンラインでご確認ください。"
},
"freeRooms": {
"freeRooms": "空いている部屋",
"detail": "占有されていない部屋を表示するために期間を選択してください。",
"searchByRoom": "部屋で検索",
"pleaseSelectDate": "日付を選択してください",
"room": "部屋",
"search": "検索",
"viewOccupancy": "占有状況を見る"
},
"moduleSelection": {
"selectAll": "すべて選択",
"deselectAll": "すべて選択解除",
"selected": "選択済み",
"unselected": "未選択",
"noModulesAvailable": "利用可能なモジュールがありません",
"modules": "モジュール"
},
"moduleInformation": {
"course": "コース",
"person": "講師",
"semester": "学期",
"module": "モジュール",
"notes": "メモ",
"day": "日",
"start": "開始",
"end": "終了",
"room": "部屋",
"type": "タイプ",
"week": "週",
"nthWeek": "{count}週",
"weekday": {
"Montag": "月曜日",
"Dienstag": "火曜日",
"Mittwoch": "水曜日",
"Donnerstag": "木曜日",
"Freitag": "金曜日",
"Samstag": "土曜日",
"Sonntag": "日曜日"
}
},
"editCalendarView": {
"error": "エラー",
"invalidToken": "無効なトークン",
"headline": "HTWカレンダーを編集",
"subTitle": "リンクまたはカレントークンを入力してください",
"loadCalendar": "カレンダーを読み込む",
"noCalendarFound": "カレンダーが見つかりません",
"save": "保存",
"delete": "削除",
"addModules": "モジュールを追加",
"dialog": {
"headline": "カレンダーを削除",
"subTitle": "カレンダーを削除してもよろしいですか?この操作は元に戻せません。カレンダーアプリからカレンダーを削除してください。",
"delete": "削除"
}, },
"roomFinderPage": { "toast": {
"roomSchedule": "部屋の占有状況", "success": "成功",
"headline": "部屋の占有計画",
"detail": "占有状況を表示するために部屋を選択してください。",
"dropDownSelect": "部屋を選択してください",
"noRoomsAvailable": "利用可能な部屋がありません",
"available": "利用可能",
"occupied": "占有中",
"stub": "オンラインでご確認ください。"
},
"freeRooms": {
"freeRooms": "空いている部屋",
"detail": "占有されていない部屋を表示するために期間を選択してください。",
"searchByRoom": "部屋で検索",
"pleaseSelectDate": "日付を選択してください",
"room": "部屋",
"search": "検索",
"viewOccupancy": "占有状況を見る"
},
"moduleSelection": {
"selectAll": "すべて選択",
"deselectAll": "すべて選択解除",
"selected": "選択済み",
"unselected": "未選択",
"noModulesAvailable": "利用可能なモジュールがありません",
"modules": "モジュール"
},
"moduleInformation": {
"course": "コース",
"person": "講師",
"semester": "学期",
"module": "モジュール",
"notes": "メモ",
"day": "日",
"start": "開始",
"end": "終了",
"room": "部屋",
"type": "タイプ",
"week": "週",
"nthWeek": "{count}週",
"weekday": {
"Montag": "月曜日",
"Dienstag": "火曜日",
"Mittwoch": "水曜日",
"Donnerstag": "木曜日",
"Freitag": "金曜日",
"Samstag": "土曜日",
"Sonntag": "日曜日"
}
},
"editCalendarView": {
"error": "エラー", "error": "エラー",
"invalidToken": "無効なトークン", "successDetail": "カレンダーが正常に削除されました",
"headline": "HTWカレンダーを編集", "errorDetail": "カレンダーを削除できませんでした"
"subTitle": "リンクまたはカレントークンを入力してください", }
"loadCalendar": "カレンダーを読み込む", },
"noCalendarFound": "カレンダーが見つかりません", "additionalModules": {
"save": "保存", "subTitle": "コースの通常の学期には記載されていない追加モジュールを選択してください",
"delete": "削除", "dropDown": "追加モジュールを選択",
"addModules": "モジュールを追加", "module": "モジュール",
"dialog": { "modules": "モジュール",
"headline": "カレンダーを削除", "footerModulesSelected": "{count}個のモジュールが選択されました",
"subTitle": "カレンダーを削除してもよろしいですか?この操作は元に戻せません。カレンダーアプリからカレンダーを削除してください。", "nextStep": "次のステップ",
"delete": "削除" "professor": "教授",
"course": "コース",
"info": "情報",
"info-long": "情報",
"paginator": {
"from": "",
"to": " から ",
"of": " の "
},
"eventType": "イベントタイプ"
},
"renameModules": {
"reminder": "リマインダー",
"enableAllNotifications": "すべての通知を有効にする",
"subTitle": "選択したモジュールを好みに合わせて設定",
"nextStep": "保存",
"error": "エラー",
"TooManyRequests": "短時間でのカレンダー作成リクエストが多すぎます"
},
"moduleTemplateDialog": {
"explanationOne": "ここで、モジュールを好みに合わせて名前を変更できます。これはカレンダーのイベント名になります。",
"explanationTwo": "さらに、各モジュールの通知を切り替えることができます。有効にすると、イベント開始の15分前に通知されます。",
"tableDescription": "モジュール名に次のプレースホルダーを使用できます:",
"placeholder": "プレースホルダー",
"description": "説明",
"examples": "例",
"moduleConfiguration": "モジュール設定",
"mandatory": "必修",
"optional": "選択",
"lecture": "講義",
"seminar": "セミナー",
"exam": "試験/インターンシッププロジェクト",
"eventTyp": "イベントタイプ"
},
"calendarLink": {
"copyToastNotification": "リンクをクリップボードにコピーしました",
"copyToastSummary": "情報",
"copyToastError": "エラー",
"copyToastErrorDetail": "リンクをクリップボードにコピーできませんでした",
"copyToClipboard": "クリップボードにコピー",
"toGoogleCalendar": "Googleカレンダー",
"toMicrosoftCalendar": "Microsoftカレンダー",
"toHTWKalendar": "HTWカレンダー"
},
"calendarPreview": {
"preview": "プレビュー",
"preview-long": "カレンダープレビュー",
"module": "モジュール",
"course": "コース"
},
"faqView": {
"headline": "よくある質問",
"firstQuestion": "HTWカレンダーを使用してカレンダーを作成するにはどうすればよいですか",
"firstAnswer": "このウェブサイトを使用すると、HTWKの時間割をお気に入りのカレンダープログラムOutlook、Googleカレンダーなどに統合できます。",
"secondQuestion": "具体的にはどのように機能しますか?",
"secondAnswer": "学習コースと希望する学期を選択し、それに関連するモジュールを選択できます。「祝日」や履修しない選択科目などのモジュールを個別に選択または選択解除できます。最後のステップで、作成されたカレンダーの対応するトークンを含むリンクが表示されます。このリンクを使用して、カレンダーを購読またはダウンロードできます。",
"thirdQuestion": "カレンダーを購読するにはどうすればよいですか?",
"thirdAnswer": {
"tabTitle": "Googleカレンダー",
"google": {
"first": "カレンダーを作成してリンクをコピーします。",
"second": "Googleカレンダーの左側のサイドバーに「その他のカレンダー」というセクションがあります。そこにあるテキストの右側にある小さな矢印アイコンをクリックします。表示されるメニューで「URLで追加」をクリックします。",
"third": "コピーしたカレンダーリンクを貼り付けて「カレンダーを追加」をクリックすると完了です。"
}, },
"toast": { "microsoft_outlook": {
"success": "成功", "title": "Microsoft Outlook",
"error": "エラー", "outlook_2010": {
"successDetail": "カレンダーが正常に削除されました", "title": "Outlook 2010で:",
"errorDetail": "カレンダーを削除できませんでした"
}
},
"additionalModules": {
"subTitle": "コースの通常の学期には記載されていない追加モジュールを選択してください",
"dropDown": "追加モジュールを選択",
"module": "モジュール",
"modules": "モジュール",
"footerModulesSelected": "{count}個のモジュールが選択されました",
"nextStep": "次のステップ",
"professor": "教授",
"course": "コース",
"info": "情報",
"info-long": "情報",
"paginator": {
"from": "",
"to": " から ",
"of": " の "
},
"eventType": "イベントタイプ"
},
"renameModules": {
"reminder": "リマインダー",
"enableAllNotifications": "すべての通知を有効にする",
"subTitle": "選択したモジュールを好みに合わせて設定",
"nextStep": "保存",
"error": "エラー",
"TooManyRequests": "短時間でのカレンダー作成リクエストが多すぎます"
},
"moduleTemplateDialog": {
"explanationOne": "ここで、モジュールを好みに合わせて名前を変更できます。これはカレンダーのイベント名になります。",
"explanationTwo": "さらに、各モジュールの通知を切り替えることができます。有効にすると、イベント開始の15分前に通知されます。",
"tableDescription": "モジュール名に次のプレースホルダーを使用できます:",
"placeholder": "プレースホルダー",
"description": "説明",
"examples": "例",
"moduleConfiguration": "モジュール設定",
"mandatory": "必修",
"optional": "選択",
"lecture": "講義",
"seminar": "セミナー",
"exam": "試験/インターンシッププロジェクト",
"eventTyp": "イベントタイプ"
},
"calendarLink": {
"copyToastNotification": "リンクをクリップボードにコピーしました",
"copyToastSummary": "情報",
"copyToastError": "エラー",
"copyToastErrorDetail": "リンクをクリップボードにコピーできませんでした",
"copyToClipboard": "クリップボードにコピー",
"toGoogleCalendar": "Googleカレンダーへ",
"toMicrosoftCalendar": "Microsoftカレンダーへ"
},
"calendarPreview": {
"preview": "プレビュー",
"preview-long": "カレンダープレビュー",
"module": "モジュール",
"course": "コース"
},
"faqView": {
"headline": "よくある質問",
"firstQuestion": "HTWカレンダーを使用してカレンダーを作成するにはどうすればよいですか",
"firstAnswer": "このウェブサイトを使用すると、HTWKの時間割をお気に入りのカレンダープログラムOutlook、Googleカレンダーなどに統合できます。",
"secondQuestion": "具体的にはどのように機能しますか?",
"secondAnswer": "学習コースと希望する学期を選択し、それに関連するモジュールを選択できます。「祝日」や履修しない選択科目などのモジュールを個別に選択または選択解除できます。最後のステップで、作成されたカレンダーの対応するトークンを含むリンクが表示されます。このリンクを使用して、カレンダーを購読またはダウンロードできます。",
"thirdQuestion": "カレンダーを購読するにはどうすればよいですか?",
"thirdAnswer": {
"tabTitle": "Googleカレンダー",
"google": {
"first": "カレンダーを作成してリンクをコピーします。", "first": "カレンダーを作成してリンクをコピーします。",
"second": "Googleカレンダーの左側のサイドバーに「その他のカレンダー」というセクションがあります。そこにあるテキストの右側にある小さな矢印アイコンをクリックします。表示されるメニューで「URLで追加」をクリックします。", "second": "「ホーム」タブをクリックします。",
"third": "コピーしたカレンダーリンクを貼り付けて「カレンダーを追加」をクリックすると完了です。" "third": "「カレンダーの管理」セクションに「カレンダーを開く」ボタンがあります。",
"fourth": "表示されるコンテキストメニューで「インターネットから」をクリックします。",
"fifth": "表示されるウィンドウにリンクを貼り付け、「OK」をクリックして確認します。追加の手順で購読を確認する必要があるかもしれません。これで完了です。"
}, },
"microsoft_outlook": { "outlook_2007": {
"title": "Microsoft Outlook", "title": "Outlook 2007で:",
"outlook_2010": {
"title": "Outlook 2010で:",
"first": "カレンダーを作成してリンクをコピーします。",
"second": "「ホーム」タブをクリックします。",
"third": "「カレンダーの管理」セクションに「カレンダーを開く」ボタンがあります。",
"fourth": "表示されるコンテキストメニューで「インターネットから」をクリックします。",
"fifth": "表示されるウィンドウにリンクを貼り付け、「OK」をクリックして確認します。追加の手順で購読を確認する必要があるかもしれません。これで完了です。"
},
"outlook_2007": {
"title": "Outlook 2007で:",
"first": "カレンダーを作成してリンクをコピーします。",
"second": "「ツール」メニューで「アカウント設定」を見つけます。",
"third": "「インターネットカレンダー」タブをクリックします。",
"fourth": "「新規」をクリックします。",
"fifth": "表示されるウィンドウにリンクを貼り付けます。ただし、「http://」を「webcal://」に置き換える必要があります。",
"sixth": "少なくとも、次回の起動時にカレンダーが更新され、利用可能になります。"
}
},
"apple_osx": {
"title": "カレンダーOS X",
"first": "カレンダーを作成してリンクをコピーします。", "first": "カレンダーを作成してリンクをコピーします。",
"second": "「ファイル」メニューで「新規カレンダー購読」を見つけますSnow Leopardでは「購読」。", "second": "「ツール」メニューで「アカウント設定」を見つけます。",
"third": "表示されるウィンドウにコピーしたリンクを貼り付け、「購読」をクリックします。", "third": "「インターネットカレンダー」タブをクリックします。",
"fourth": "カレンダーに名前を付け、更新頻度を設定できます。iPhoneなどでiCloudを使用している場合、「場所」の下に「iCloud」を選択することをお勧めします。これにより、追加の操作なしで常に予定表が利用可能になります。" "fourth": "「新規」をクリックします。",
}, "fifth": "表示されるウィンドウにリンクを貼り付けます。ただし、「http://」を「webcal://」に置き換える必要があります。",
"thunderbird": { "sixth": "少なくとも、次回の起動時にカレンダーが更新され、利用可能になります。"
"title": "Thunderbird",
"one": "カレンダーを作成してリンクをコピーします。",
"two": "「イベントとタスク」メニューで「カレンダー」を選択します。",
"three": "左側にカレンダーの概要が表示されます。この領域で右クリックし、表示されるコンテキストメニューで「新しいカレンダー」をクリックします。",
"four": "「コンピューター上」または「ネットワーク上」を選択できます。後者を選択して「続行」をクリックします。",
"five": "次のウィンドウで「形式」を「iCalendar」のままにします。",
"six": "「場所」にコピーしたカレンダーリンクを貼り付けます。",
"seven": "名前を付け、必要に応じて追加の設定を行えます。"
},
"iphone": {
"title": "iPhone",
"description": "iOSで最も簡単な方法は、iCloud同期を通じて行う方法ですOS Xカレンダーの指示を参照。別の方法もあります:",
"one": "カレンダーを作成してリンクをコピーします。",
"two": "「設定」に移動します。",
"three": "「メール、連絡先、カレンダー」を選択します。",
"four": "「アカウント追加」を選択します。",
"five": "下部にある「その他」をタップします。",
"six": "「購読カレンダーを追加」が最後のオプションです。それをタップします。",
"seven": "表示されるテキストフィールドにカレンダーリンクを貼り付け、上部の「次へ」を押します。",
"eight": "「説明」を入力できます。その他の設定はそのままにします。",
"nine": "しばらくすると、購読カレンダーがカレンダーアプリに表示されます。"
},
"android": {
"description": "Androidでは、Googleカレンダーとの同期が最も簡単なオプションです。Googleカレンダーの指示を参照してカレンダーを購読する方法を確認してください。"
},
"windows_phone": {
"description": "Windows Phoneでは、Outlook.comを通じた同期が最も簡単な方法です:",
"one": "カレンダーを作成してリンクをコピーします。",
"two": "Outlook.comにログインし、カレンダーに移動します。",
"three": "「購読」をクリックします。",
"four": "「公開カレンダーを購読」を選択します。",
"five": "「カレンダーURL」にカレンダーリンクを貼り付けます。",
"six": "他の設定を希望に応じて行います。",
"seven": "「カレンダーを購読」をクリックします。",
"eight": "Windows Phoneデバイスは同じOutlook.comユーザーアカウントでログインしている必要があります。この後、カレンダーの同期が自動的に行われるはずです。"
} }
}, },
"fourthQuestion": "カレンダーを購読する?ダウンロードしたい!", "apple_osx": {
"fourthAnswer": "もちろん可能です。個人の時間割が作成された後、ダウンロードするオプションがあります。また、生成されたリンクをブラウザで開くだけでいつでもダウンロードできます。ダウンロードしたカレンダーや時間割は更新されないことを覚えておいてください。更新はカレンダーを購読する場合にのみ可能です。", "title": "カレンダーOS X",
"fifthQuestion": "他の学科の追加モジュールを履修し、それを時間割に追加したい。", "first": "カレンダーを作成してリンクをコピーします。",
"fifthAnswer": "モジュールハンドブックからコースに提供されているモジュールを選択する機会があった後、2ページ目にリダイレクトされます。そこでは、他の学科から追加のモジュールを学習計画に追加することができます。", "second": "「ファイル」メニューで「新規カレンダー購読」を見つけますSnow Leopardでは「購読」。",
"sixthQuestion": "カレンダーの更新に問題がありますか?", "third": "表示されるウィンドウにコピーしたリンクを貼り付け、「購読」をクリックします。",
"sixthAnswer": "これは、おそらくダウンロードしたためであり、購読していないためです。購読したカレンダーのみが自動的に更新されます。サーバーのカレンダー更新は毎日0時から3時間ごとに行われます。これにより、HTWKからのすべての変更が反映されます。", "fourth": "カレンダーに名前を付け、更新頻度を設定できます。iPhoneなどでiCloudを使用している場合、「場所」の下に「iCloud」を選択することをお勧めします。これにより、追加の操作なしで常に予定表が利用可能になります。"
"seventhQuestion": "私の時間割やそのリンクはどのくらい有効ですか?", },
"seventhAnswer": "時間割は選択された学期のみに有効です。選択科目や計画の変更による影響があるためです。しかし、リンクは無期限に有効であり、いつでも時間割を更新できます。", "thunderbird": {
"eighthQuestion": "費用と開発?", "title": "Thunderbird",
"eighthAnswer": "開発はコミュニティによるアクティブなGitプロジェクトとして管理されるべきです。HTWカレンダーの無料バージョンはFSR IMNのサーバーに内部でホストされ、すべてのHTWK学生に無料で提供されます。", "one": "カレンダーを作成してリンクをコピーします。",
"ninthQuestion": "ソースコードはどこにありますか?", "two": "「イベントとタスク」メニューで「カレンダー」を選択します。",
"ninthAnswer": "貢献したい場合は、HTWKの学生であればいつでもできます。ソースコードは以下にあります", "three": "左側にカレンダーの概要が表示されます。この領域で右クリックし、表示されるコンテキストメニューで「新しいカレンダー」をクリックします",
"notFound": "探しているものが見つかりませんか?", "four": "「コンピューター上」または「ネットワーク上」を選択できます。後者を選択して「続行」をクリックします。",
"contact": "連絡する" "five": "次のウィンドウで「形式」を「iCalendar」のままにします。",
} "six": "「場所」にコピーしたカレンダーリンクを貼り付けます。",
"seven": "名前を付け、必要に応じて追加の設定を行えます。"
},
"iphone": {
"title": "iPhone",
"description": "iOSで最も簡単な方法は、iCloud同期を通じて行う方法ですOS Xカレンダーの指示を参照。別の方法もあります:",
"one": "カレンダーを作成してリンクをコピーします。",
"two": "「設定」に移動します。",
"three": "「メール、連絡先、カレンダー」を選択します。",
"four": "「アカウント追加」を選択します。",
"five": "下部にある「その他」をタップします。",
"six": "「購読カレンダーを追加」が最後のオプションです。それをタップします。",
"seven": "表示されるテキストフィールドにカレンダーリンクを貼り付け、上部の「次へ」を押します。",
"eight": "「説明」を入力できます。その他の設定はそのままにします。",
"nine": "しばらくすると、購読カレンダーがカレンダーアプリに表示されます。"
},
"android": {
"description": "Androidでは、Googleカレンダーとの同期が最も簡単なオプションです。Googleカレンダーの指示を参照してカレンダーを購読する方法を確認してください。"
},
"windows_phone": {
"description": "Windows Phoneでは、Outlook.comを通じた同期が最も簡単な方法です:",
"one": "カレンダーを作成してリンクをコピーします。",
"two": "Outlook.comにログインし、カレンダーに移動します。",
"three": "「購読」をクリックします。",
"four": "「公開カレンダーを購読」を選択します。",
"five": "「カレンダーURL」にカレンダーリンクを貼り付けます。",
"six": "他の設定を希望に応じて行います。",
"seven": "「カレンダーを購読」をクリックします。",
"eight": "Windows Phoneデバイスは同じOutlook.comユーザーアカウントでログインしている必要があります。この後、カレンダーの同期が自動的に行われるはずです。"
}
},
"fourthQuestion": "カレンダーを購読する?ダウンロードしたい!",
"fourthAnswer": "もちろん可能です。個人の時間割が作成された後、ダウンロードするオプションがあります。また、生成されたリンクをブラウザで開くだけでいつでもダウンロードできます。ダウンロードしたカレンダーや時間割は更新されないことを覚えておいてください。更新はカレンダーを購読する場合にのみ可能です。",
"fifthQuestion": "他の学科の追加モジュールを履修し、それを時間割に追加したい。",
"fifthAnswer": "モジュールハンドブックからコースに提供されているモジュールを選択する機会があった後、2ページ目にリダイレクトされます。そこでは、他の学科から追加のモジュールを学習計画に追加することができます。",
"sixthQuestion": "カレンダーの更新に問題がありますか?",
"sixthAnswer": "これは、おそらくダウンロードしたためであり、購読していないためです。購読したカレンダーのみが自動的に更新されます。サーバーのカレンダー更新は毎日0時から3時間ごとに行われます。これにより、HTWKからのすべての変更が反映されます。",
"seventhQuestion": "私の時間割やそのリンクはどのくらい有効ですか?",
"seventhAnswer": "時間割は選択された学期のみに有効です。選択科目や計画の変更による影響があるためです。しかし、リンクは無期限に有効であり、いつでも時間割を更新できます。",
"eighthQuestion": "費用と開発?",
"eighthAnswer": "開発はコミュニティによるアクティブなGitプロジェクトとして管理されるべきです。HTWカレンダーの無料バージョンはFSR IMNのサーバーに内部でホストされ、すべてのHTWK学生に無料で提供されます。",
"ninthQuestion": "ソースコードはどこにありますか?",
"ninthAnswer": "貢献したい場合は、HTWKの学生であればいつでもできます。ソースコードは以下にあります",
"notFound": "探しているものが見つかりませんか?",
"contact": "連絡する"
},
"userCalender": {
"headline": "ユーザーカレンダー",
"subTitle": "ここでは、個人のフィードのカレンダー表示を見つけることができます。",
"searchPlaceholder": "カレンダートークン",
"searchButton": "ロードカレンダー"
} }
}

View File

@ -56,12 +56,15 @@ import Calendar from "primevue/calendar";
import i18n from "./i18n"; import i18n from "./i18n";
import { VueQueryPlugin } from "@tanstack/vue-query"; import { VueQueryPlugin } from "@tanstack/vue-query";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
polyfillCountryFlagEmojis(); polyfillCountryFlagEmojis();
const app = createApp(App); const app = createApp(App);
const pinia = createPinia(); const pinia = createPinia();
pinia.use(piniaPluginPersistedstate)
app.use(VueQueryPlugin, { app.use(VueQueryPlugin, {
queryClientConfig: { queryClientConfig: {
defaultOptions: { defaultOptions: {

View File

@ -17,8 +17,8 @@
import { beforeEach, describe, expect, test } from "vitest"; import { beforeEach, describe, expect, test } from "vitest";
import { RoomOccupancyList } from "./roomOccupancyList"; import { RoomOccupancyList } from "./roomOccupancyList";
import { Binary } from "bson"; import { Binary } from "bson";
import { add, addHours, addMinutes, interval, subHours } from "date-fns"; import { addHours, addMinutes, interval, subHours } from "date-fns";
import { fromZonedTime, toZonedTime } from "date-fns-tz"; import { toZonedTime } from "date-fns-tz";
const testListStart = new Date("2022-01-01T00:00:00Z"); const testListStart = new Date("2022-01-01T00:00:00Z");
var testList : RoomOccupancyList; //= RoomOccupancyList.fromJSON({}); var testList : RoomOccupancyList; //= RoomOccupancyList.fromJSON({});

View File

@ -28,6 +28,7 @@ const EditAdditionalModules = () =>
const EditModules = () => import("../view/editCalendar/EditModules.vue"); const EditModules = () => import("../view/editCalendar/EditModules.vue");
const CourseSelection = () => import("../view/CourseSelection.vue"); const CourseSelection = () => import("../view/CourseSelection.vue");
const FreeRooms = () => import("../view/FreeRooms.vue"); const FreeRooms = () => import("../view/FreeRooms.vue");
const CalenderViewer = () => import("../view/UserCalendar.vue");
import i18n from "../i18n"; import i18n from "../i18n";
@ -36,7 +37,12 @@ const router = createRouter({
routes: [ routes: [
{ {
path: "/", path: "/",
name: "course-selection", name: "home",
component: CourseSelection,
},
{
path: "/calendar/create",
name: "calendar-create",
component: CourseSelection, component: CourseSelection,
}, },
{ {
@ -54,6 +60,11 @@ const router = createRouter({
name: "free-rooms", name: "free-rooms",
component: FreeRooms, component: FreeRooms,
}, },
{
path: "/calendar/view",
name: "calendar-view",
component: CalenderViewer,
},
{ {
path: "/faq", path: "/faq",
name: "faq", name: "faq",
@ -80,7 +91,7 @@ const router = createRouter({
component: CalendarLink, component: CalendarLink,
}, },
{ {
path: "/edit", path: "/calendar/edit",
name: "edit", name: "edit",
component: EditCalendarView, component: EditCalendarView,
}, },

View File

@ -20,6 +20,7 @@ const tokenStore = defineStore("tokenStore", {
state: () => ({ state: () => ({
token: "", token: "",
}), }),
persist: true,
actions: { actions: {
setToken(token: string) { setToken(token: string) {
this.token = token; this.token = token;

View File

@ -26,6 +26,7 @@ import tokenStore from "../store/tokenStore";
import { useToast } from "primevue/usetoast"; import { useToast } from "primevue/usetoast";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import DynamicPage from "./DynamicPage.vue"; import DynamicPage from "./DynamicPage.vue";
import { extractToken, isToken } from "@/helpers/token.ts";
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const toast = useToast(); const toast = useToast();
@ -33,26 +34,6 @@ const toast = useToast();
const token: Ref<string> = ref(""); const token: Ref<string> = ref("");
const modules: Ref<Map<string, Module>> = ref(moduleStore().modules); const modules: Ref<Map<string, Module>> = ref(moduleStore().modules);
const tokenRegex = /^[a-z0-9]{15}$/;
const tokenUriRegex = /[?&]token=([a-z0-9]{15})(?:&|$)/;
const isToken = (token: string): boolean => {
return tokenRegex.test(token) || tokenUriRegex.test(token);
};
function extractToken(token: string): string {
if (tokenRegex.test(token)) {
return token;
}
const match = tokenUriRegex.exec(token);
if (match) {
return match[1];
}
throw new Error("Invalid token");
}
function loadCalendar(): void { function loadCalendar(): void {
try { try {
token.value = extractToken(token.value); token.value = extractToken(token.value);

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import CalendarViewer from "@/components/CalendarViewer.vue";
import DynamicPage from "@/view/DynamicPage.vue";
import { useI18n } from "vue-i18n";
import { onMounted, ref } from "vue";
import { extractToken } from "@/helpers/token.ts";
import { useToast } from "primevue/usetoast";
import moduleStore from "@/store/moduleStore.ts";
import tokenStore from "@/store/tokenStore.ts";
const { t } = useI18n({ useScope: "global" });
const toast = useToast();
const token = ref(tokenStore().token || "" as string );
// parse token from query parameter
const urlParams = new URLSearchParams(window.location.search);
const tokenFromUrl = urlParams.get("token");
if (tokenFromUrl) {
token.value = tokenFromUrl;
tokenStore().setToken(tokenFromUrl);
loadCalendar();
}
function loadCalendar() {
try {
token.value = extractToken(token.value);
} catch (e) {
console.error(e);
toast.add({
severity: "error",
summary: t("editCalendarView.error"),
detail: t("editCalendarView.invalidToken"),
life: 3000,
});
return;
}
moduleStore().removeAllModules();
tokenStore().setToken(token.value);
}
onMounted(() => {
if (token.value && token.value !== "") {
loadCalendar();
}
});
</script>
<template>
<DynamicPage
:hide-content="false"
:headline="$t('userCalender.headline')"
:sub-title="$t('userCalender.subTitle')"
>
<template #selection="{ flexSpecs }">
<InputText
v-model="token"
:placeholder="$t('userCalender.searchPlaceholder')"
:class="flexSpecs"
@keyup.enter="loadCalendar()"
/>
<Button
:label="$t('userCalender.searchButton')"
icon="pi pi-refresh"
@click="loadCalendar()"
/>
</template>
<template #content>
<CalendarViewer
:token="tokenStore().token"
/>
</template>
</DynamicPage>
</template>
<style scoped>
</style>

View File

@ -15,3 +15,5 @@
//along with this program. If not, see <https://www.gnu.org/licenses/>. //along with this program. If not, see <https://www.gnu.org/licenses/>.
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module 'ical.js';

View File

@ -18,12 +18,12 @@ import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
import mkcert from 'vite-plugin-mkcert'; import basicSsl from '@vitejs/plugin-basic-ssl'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
mkcert(), basicSsl(),
VitePWA({ VitePWA({
mode: 'development', mode: 'development',
base: '/', base: '/',
@ -67,7 +67,21 @@ export default defineConfig({
registerType: 'autoUpdate', registerType: 'autoUpdate',
workbox: { workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,vue,txt,woff2}'], globPatterns: ['**/*.{js,css,html,ico,png,svg,json,vue,txt,woff2}'],
cleanupOutdatedCaches: true cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /^https?.*/,
handler: 'NetworkFirst',
options: {
cacheName: 'https-calls',
expiration: {
maxEntries: 150,
maxAgeSeconds: 30 * 12 * 60 * 60, // 1 month
},
networkTimeoutSeconds: 10, // fall back to cache if api does not response within 10 seconds
},
},
],
}, },
devOptions: { devOptions: {
enabled: true, enabled: true,

View File

@ -113,60 +113,30 @@ http {
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name cal.htwk-leipzig.de; server_name pwa.htwkalender.de;
location /api/feed {
proxy_pass http://htwkalender-backend:8090;
client_max_body_size 2m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
limit_req zone=feed burst=10 nodelay;
}
location / { location / {
return 301 https://cal.htwk-leipzig.de$request_uri; return 301 https://$host$request_uri;
}
}
server {
listen 80;
listen [::]:80;
server_name htwkalender.de;
location /api/feed {
proxy_pass http://htwkalender-backend:8090;
client_max_body_size 2m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
limit_req zone=feed burst=10 nodelay;
}
location / {
return 301 https://cal.htwk-leipzig.de$request_uri;
} }
} }
server { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
server_name htwkalender.de www.htwkalender.de; server_name pwa.htwkalender.de;
ssl_certificate htwkalender.de.pem; ssl_certificate htwkalender.de.pem;
ssl_certificate_key htwkalender.de.key.pem; ssl_certificate_key htwkalender.de.key.pem;
return 301 https://cal.htwk-leipzig.de$request_uri;
}
server { location /_ {
listen 443 ssl; proxy_pass http://htwkalender-backend:8090;
listen [::]:443 ssl; # if user is not 0 in admin list, return 404
server_name cal.htwk-leipzig.de; if ($admin) {
return 404 "Not Found";
ssl_certificate cal.htwk-leipzig.de.pem; }
ssl_certificate_key cal.htwk-leipzig.de.key.pem; # Increase upload file size
client_max_body_size 100m;
}
location /api { location /api {
proxy_pass http://htwkalender-backend:8090; proxy_pass http://htwkalender-backend:8090;
@ -272,16 +242,6 @@ http {
send_timeout 600s; send_timeout 600s;
} }
location /_ {
proxy_pass http://htwkalender-backend:8090;
# if user is not 0 in admin list, return 404
if ($admin) {
return 404 "Not Found";
}
# Increase upload file size
client_max_body_size 100m;
}
location / { location / {
proxy_pass http://htwkalender-frontend:8000; proxy_pass http://htwkalender-frontend:8000;
} }

View File

@ -51,25 +51,8 @@ http {
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name dev.htwkalender.de;
location /api { return 301 https://$host$request_uri;
proxy_pass http://htwkalender-backend:8090;
client_max_body_size 20m;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
}
location /_ {
proxy_pass http://htwkalender-backend:8090;
# Increase upload file size
client_max_body_size 100m;
}
location / {
proxy_pass http://htwkalender-frontend:8000;
}
} }
server { server {