mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-07-25 05:49:15 +02:00
Merge branch '4-offline-calendar-ical-viewer' into 'main'
Resolve "offline calendar ical viewer" Closes #4 See merge request htwk-software/htwkalender-pwa!2
This commit is contained in:
@@ -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:
|
||||||
|
@@ -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"
|
||||||
|
2220
frontend/package-lock.json
generated
2220
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,39 +13,43 @@
|
|||||||
"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",
|
||||||
"country-flag-emoji-polyfill": "^0.1.8",
|
"country-flag-emoji-polyfill": "^0.1.8",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
```
|
```
|
||||||
|
@@ -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*/
|
||||||
|
@@ -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*/
|
||||||
|
@@ -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
@@ -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
23
frontend/src/api/loadICal.ts
Normal file
23
frontend/src/api/loadICal.ts
Normal 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();
|
||||||
|
}
|
@@ -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>
|
||||||
|
|
||||||
|
133
frontend/src/components/CalendarViewer.vue
Normal file
133
frontend/src/components/CalendarViewer.vue
Normal 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>
|
@@ -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"),
|
||||||
|
@@ -172,9 +172,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",
|
||||||
|
24
frontend/src/helpers/ical.ts
Normal file
24
frontend/src/helpers/ical.ts
Normal 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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
19
frontend/src/helpers/token.ts
Normal file
19
frontend/src/helpers/token.ts
Normal 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");
|
||||||
|
}
|
@@ -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",
|
||||||
@@ -137,7 +139,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",
|
||||||
@@ -238,5 +241,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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",
|
||||||
@@ -137,7 +139,8 @@
|
|||||||
"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": "to Google Calendar",
|
||||||
"toMicrosoftCalendar": "to Microsoft Calendar"
|
"toMicrosoftCalendar": "to Microsoft Calendar",
|
||||||
|
"toHTWKalendar": "HTWKalender"
|
||||||
},
|
},
|
||||||
"calendarPreview": {
|
"calendarPreview": {
|
||||||
"preview": "preview",
|
"preview": "preview",
|
||||||
@@ -238,5 +241,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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: {
|
||||||
|
@@ -27,6 +27,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";
|
||||||
|
|
||||||
@@ -35,7 +36,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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -48,6 +54,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",
|
||||||
@@ -74,7 +85,7 @@ const router = createRouter({
|
|||||||
component: CalendarLink,
|
component: CalendarLink,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/edit",
|
path: "/calendar/edit",
|
||||||
name: "edit",
|
name: "edit",
|
||||||
component: EditCalendarView,
|
component: EditCalendarView,
|
||||||
},
|
},
|
||||||
|
@@ -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;
|
||||||
|
@@ -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);
|
||||||
|
81
frontend/src/view/UserCalendar.vue
Normal file
81
frontend/src/view/UserCalendar.vue
Normal 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>
|
2
frontend/src/vite-env.d.ts
vendored
2
frontend/src/vite-env.d.ts
vendored
@@ -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';
|
@@ -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,
|
||||||
|
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user