mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-07-16 09:38:51 +02:00
Merge remote-tracking branch 'origin/main' into 22-semi-offline-room-finder
This commit is contained in:
66
frontend/package-lock.json
generated
66
frontend/package-lock.json
generated
@ -48,7 +48,9 @@
|
|||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"vite-plugin-pwa": "^0.20.0",
|
"vite-plugin-pwa": "^0.20.0",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"vue-tsc": "^1.8.27"
|
"vue-tsc": "^1.8.27",
|
||||||
|
"workbox-core": "^7.1.0",
|
||||||
|
"workbox-precaching": "^7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
@ -2981,28 +2983,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/eslint": {
|
|
||||||
"version": "8.56.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz",
|
|
||||||
"integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==",
|
|
||||||
"dev": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/estree": "*",
|
|
||||||
"@types/json-schema": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/eslint-scope": {
|
|
||||||
"version": "3.7.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
|
|
||||||
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
|
|
||||||
"dev": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/eslint": "*",
|
|
||||||
"@types/estree": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||||
@ -3824,11 +3804,12 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn-import-assertions": {
|
"node_modules/acorn-import-attributes": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
|
||||||
"integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
|
"integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"acorn": "^8"
|
"acorn": "^8"
|
||||||
@ -4631,10 +4612,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.16.1",
|
"version": "5.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||||
"integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==",
|
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.4",
|
"graceful-fs": "^4.2.4",
|
||||||
@ -6329,10 +6311,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.3",
|
"braces": "^3.0.3",
|
||||||
"picomatch": "^2.3.1"
|
"picomatch": "^2.3.1"
|
||||||
@ -7662,6 +7645,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@ -8407,22 +8391,22 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.91.0",
|
"version": "5.94.0",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
|
||||||
"integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==",
|
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.3",
|
|
||||||
"@types/estree": "^1.0.5",
|
"@types/estree": "^1.0.5",
|
||||||
"@webassemblyjs/ast": "^1.12.1",
|
"@webassemblyjs/ast": "^1.12.1",
|
||||||
"@webassemblyjs/wasm-edit": "^1.12.1",
|
"@webassemblyjs/wasm-edit": "^1.12.1",
|
||||||
"@webassemblyjs/wasm-parser": "^1.12.1",
|
"@webassemblyjs/wasm-parser": "^1.12.1",
|
||||||
"acorn": "^8.7.1",
|
"acorn": "^8.7.1",
|
||||||
"acorn-import-assertions": "^1.9.0",
|
"acorn-import-attributes": "^1.9.5",
|
||||||
"browserslist": "^4.21.10",
|
"browserslist": "^4.21.10",
|
||||||
"chrome-trace-event": "^1.0.2",
|
"chrome-trace-event": "^1.0.2",
|
||||||
"enhanced-resolve": "^5.16.0",
|
"enhanced-resolve": "^5.17.1",
|
||||||
"es-module-lexer": "^1.2.1",
|
"es-module-lexer": "^1.2.1",
|
||||||
"eslint-scope": "5.1.1",
|
"eslint-scope": "5.1.1",
|
||||||
"events": "^3.2.0",
|
"events": "^3.2.0",
|
||||||
@ -8806,7 +8790,8 @@
|
|||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.1.0.tgz",
|
||||||
"integrity": "sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==",
|
"integrity": "sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/workbox-expiration": {
|
"node_modules/workbox-expiration": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
@ -8844,6 +8829,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.1.0.tgz",
|
||||||
"integrity": "sha512-LyxzQts+UEpgtmfnolo0hHdNjoB7EoRWcF7EDslt+lQGd0lW4iTvvSe3v5JiIckQSB5KTW5xiCqjFviRKPj1zA==",
|
"integrity": "sha512-LyxzQts+UEpgtmfnolo0hHdNjoB7EoRWcF7EDslt+lQGd0lW4iTvvSe3v5JiIckQSB5KTW5xiCqjFviRKPj1zA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"workbox-core": "7.1.0",
|
"workbox-core": "7.1.0",
|
||||||
"workbox-routing": "7.1.0",
|
"workbox-routing": "7.1.0",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "htwkalender",
|
"name": "htwkalender",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@ -53,6 +53,8 @@
|
|||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"vite-plugin-pwa": "^0.20.0",
|
"vite-plugin-pwa": "^0.20.0",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"vue-tsc": "^1.8.27"
|
"vue-tsc": "^1.8.27",
|
||||||
|
"workbox-core": "^7.1.0",
|
||||||
|
"workbox-precaching": "^7.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
63
frontend/public/sw.js
Normal file
63
frontend/public/sw.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { precacheAndRoute, cleanupOutdatedCaches } from "workbox-precaching";
|
||||||
|
import { clientsClaim } from "workbox-core";
|
||||||
|
|
||||||
|
self.skipWaiting();
|
||||||
|
clientsClaim();
|
||||||
|
|
||||||
|
cleanupOutdatedCaches();
|
||||||
|
|
||||||
|
// Precache the files from the manifest
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
// Custom precaching logic for the /api/modules endpoint
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open("api-modules-cache").then((cache) => {
|
||||||
|
return fetch("/api/modules")
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return cache.put("/api/modules", response);
|
||||||
|
}
|
||||||
|
throw new Error("Failed to fetch /api/modules");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Precaching /api/modules failed:", error);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom precaching logic for the /api/schedule/rooms endpoint
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open("room-schedule-cache").then((cache) => {
|
||||||
|
return fetch("/api/schedule/rooms")
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return cache.put("/api/schedule/rooms", response);
|
||||||
|
}
|
||||||
|
throw new Error("Failed to fetch /api/schedule/rooms");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Precaching /api/schedule/rooms failed:", error);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// StaleWhileRevalidate strategy for the /api/schedule/rooms endpoint
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
if (event.request.url.includes("/api/schedule/rooms")) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.open("room-schedule-cache").then((cache) => {
|
||||||
|
return cache.match(event.request).then((response) => {
|
||||||
|
const fetchPromise = fetch(event.request).then((networkResponse) => {
|
||||||
|
cache.put(event.request, networkResponse.clone());
|
||||||
|
return networkResponse;
|
||||||
|
});
|
||||||
|
return response || fetchPromise;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -15,7 +15,8 @@
|
|||||||
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { Module } from "../model/module";
|
import { Module } from "../model/module";
|
||||||
import { Calendar } from "../model/calendar";
|
import tokenStore from "@/store/tokenStore.ts";
|
||||||
|
import { Calendar } from "@/model/calendar.ts";
|
||||||
|
|
||||||
export async function getCalender(token: string): Promise<Module[]> {
|
export async function getCalender(token: string): Promise<Module[]> {
|
||||||
const request = new Request("/api/collections/feeds/records/" + token, {
|
const request = new Request("/api/collections/feeds/records/" + token, {
|
||||||
@ -24,9 +25,10 @@ export async function getCalender(token: string): Promise<Module[]> {
|
|||||||
|
|
||||||
return await fetch(request).then((response) => {
|
return await fetch(request).then((response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response
|
return response.json().then((calendarResponse: Calendar) => {
|
||||||
.json()
|
tokenStore().feed = calendarResponse;
|
||||||
.then((calendarResponse: Calendar) => calendarResponse.modules);
|
return calendarResponse.modules;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
9
frontend/src/components/AppVersion.vue
Normal file
9
frontend/src/components/AppVersion.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const version = __APP_VERSION__;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p>
|
||||||
|
{{ version }}
|
||||||
|
</p>
|
||||||
|
</template>
|
@ -84,28 +84,78 @@ const forwardToHTWKalendar = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const actions = computed(() => [
|
const shareData = computed(() => ({
|
||||||
{
|
title: t("calendarLink.shareTitle"),
|
||||||
label: t("calendarLink.copyToClipboard"),
|
text: t("calendarLink.shareText") + getLink(),
|
||||||
icon: "pi pi-copy",
|
url: "https://" + domain + "/",
|
||||||
command: copyToClipboard,
|
}));
|
||||||
},
|
|
||||||
{
|
const shareLink = () => {
|
||||||
label: t("calendarLink.toGoogleCalendar"),
|
if (typeof navigator.share === 'function' && navigator.canShare(shareData.value)) {
|
||||||
icon: "pi pi-google",
|
navigator
|
||||||
command: forwardToGoogle,
|
.share(shareData.value)
|
||||||
},
|
.then(() => {
|
||||||
{
|
toast.add({
|
||||||
label: t("calendarLink.toMicrosoftCalendar"),
|
severity: "info",
|
||||||
icon: "pi pi-microsoft",
|
summary: t("calendarLink.shareToastSummary"),
|
||||||
command: forwardToMicrosoft,
|
detail: t("calendarLink.shareToastNotification"),
|
||||||
},
|
life: 3000,
|
||||||
{
|
});
|
||||||
label: t("calendarLink.toHTWKalendar"),
|
})
|
||||||
icon: "pi pi-home",
|
.catch(() => {
|
||||||
command: forwardToHTWKalendar,
|
toast.add({
|
||||||
},
|
severity: "error",
|
||||||
]);
|
summary: t("calendarLink.shareToastError"),
|
||||||
|
detail: t("calendarLink.shareToastErrorDetail"),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: t("calendarLink.shareToastError"),
|
||||||
|
detail: t("calendarLink.shareToastErrorDetail"),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = computed(() => {
|
||||||
|
const actionList = [
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("calendarLink.toHTWKalendar"),
|
||||||
|
icon: "pi pi-home",
|
||||||
|
command: forwardToHTWKalendar,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (typeof navigator.share === 'function' && navigator.canShare(shareData.value)) {
|
||||||
|
actionList.push({
|
||||||
|
label: t("calendarLink.share"),
|
||||||
|
icon: "pi pi-share-alt",
|
||||||
|
command: shareLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -46,12 +46,21 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type CalendarEvent = {
|
||||||
|
title: string;
|
||||||
|
start: Date | null;
|
||||||
|
end: Date | null;
|
||||||
|
notes: string;
|
||||||
|
allDay: boolean;
|
||||||
|
location: string;
|
||||||
|
};
|
||||||
|
|
||||||
const op = ref();
|
const op = ref();
|
||||||
const clickedEvent = ref();
|
const clickedEvent : Ref<CalendarEvent | null> = ref(null);
|
||||||
|
|
||||||
const toggle = (info: EventClickArg) => {
|
const toggle = (info: EventClickArg) => {
|
||||||
const start = !info.event.start ? "" : info.event.start;
|
const start = !info.event.start ? null : info.event.start;
|
||||||
const end = !info.event.end ? "" : info.event.end;
|
const end = !info.event.end ? null : info.event.end;
|
||||||
|
|
||||||
if (op.value.visible) {
|
if (op.value.visible) {
|
||||||
clickedEvent.value = null;
|
clickedEvent.value = null;
|
||||||
@ -199,12 +208,13 @@ watch(mobilePage, () => {
|
|||||||
</FullCalendar>
|
</FullCalendar>
|
||||||
|
|
||||||
<OverlayPanel ref="op">
|
<OverlayPanel ref="op">
|
||||||
<div>
|
<div v-if="clickedEvent">
|
||||||
<h3>{{ clickedEvent.title }}</h3>
|
<h3>{{ clickedEvent.title }}</h3>
|
||||||
<p>Location: {{ clickedEvent.location }}</p>
|
<p><b>{{ $t("calendarViewer.location") }}:</b> {{ clickedEvent.location }}</p>
|
||||||
<p>Start: {{ clickedEvent.start?.toLocaleString() }}</p>
|
<p><b>{{ $t("calendarViewer.start") }}:</b> {{ clickedEvent.start ? $d(clickedEvent.start, "long") : ""}}</p>
|
||||||
<p>End: {{ clickedEvent.end?.toLocaleString() }}</p>
|
<p><b>{{ $t("calendarViewer.end") }}:</b> {{ clickedEvent.end ? $d(clickedEvent.end, "long") : "" }}</p>
|
||||||
<p>Notes: {{ clickedEvent.notes }}</p>
|
<p><b>{{ $t("calendarViewer.notes") }}:</b></p>
|
||||||
|
<p v-for="note in clickedEvent.notes.split('\n')" class="note-line" :key="note">{{ note }}</p>
|
||||||
</div>
|
</div>
|
||||||
</OverlayPanel>
|
</OverlayPanel>
|
||||||
</template>
|
</template>
|
||||||
@ -215,4 +225,9 @@ watch(mobilePage, () => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-line {
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -68,12 +68,7 @@ updateLocale(settingsStore().locale);
|
|||||||
<template #value="slotProps">
|
<template #value="slotProps">
|
||||||
<div v-if="slotProps.value" class="flex align-items-center">
|
<div v-if="slotProps.value" class="flex align-items-center">
|
||||||
<div class="mr-2 flag">{{ displayIcon(slotProps.value) }}</div>
|
<div class="mr-2 flag">{{ displayIcon(slotProps.value) }}</div>
|
||||||
<div
|
<div>
|
||||||
style="
|
|
||||||
font-family: "Twemoji Country Flags",
|
|
||||||
"Helvetica", "Comic Sans", serif;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ displayCountry(slotProps.value) }}
|
{{ displayCountry(slotProps.value) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
39
frontend/src/components/ReloadPwa.vue
Normal file
39
frontend/src/components/ReloadPwa.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useToast } from "primevue/usetoast";
|
||||||
|
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const updateCache = () => {
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
registration.unregister().then((boolean) => {
|
||||||
|
if (boolean) {
|
||||||
|
Toast.add({
|
||||||
|
severity: "success",
|
||||||
|
summary: "Service Worker",
|
||||||
|
detail: "Service Worker has been unregistered",
|
||||||
|
life: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
Toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: "Service Worker",
|
||||||
|
detail: "Service Worker could not be unregistered",
|
||||||
|
life: 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button @click="updateCache()">
|
||||||
|
{{ $t("settings.reloadPwa") }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
@ -21,14 +21,14 @@ import FullCalendar from "@fullcalendar/vue3";
|
|||||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import {computed, ComputedRef, inject, ref, Ref, watch} from "vue";
|
import { computed, ComputedRef, inject, ref, Ref, watch } from "vue";
|
||||||
import {CalendarOptions, DatesSetArg, EventInput} from "@fullcalendar/core";
|
import { CalendarOptions, DatesSetArg, EventInput } from "@fullcalendar/core";
|
||||||
import {useI18n} from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import allLocales from "@fullcalendar/core/locales-all";
|
import allLocales from "@fullcalendar/core/locales-all";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import {formatYearMonthDay} from "@/helpers/dates";
|
import { formatYearMonthDay } from "@/helpers/dates";
|
||||||
import {isValid} from "date-fns";
|
import { isValid } from "date-fns";
|
||||||
import {RoomOccupancyList} from "@/model/roomOccupancyList";
|
import { RoomOccupancyList } from "@/model/roomOccupancyList";
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
@ -84,15 +84,15 @@ function transformData(data: RoomOccupancyList) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
.decodeOccupancy(
|
.decodeOccupancy(
|
||||||
selectedRoom.value,
|
selectedRoom.value,
|
||||||
new Date(currentDateFrom.value),
|
new Date(currentDateFrom.value),
|
||||||
new Date(currentDateTo.value),
|
new Date(currentDateTo.value),
|
||||||
)
|
)
|
||||||
.map((event, index) => ({
|
.map((event, index) => ({
|
||||||
id: index,
|
id: index,
|
||||||
event: event,
|
event: event,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const occupations = computed(() => {
|
const occupations = computed(() => {
|
||||||
|
@ -149,7 +149,14 @@
|
|||||||
"copyToClipboard": "Link kopieren",
|
"copyToClipboard": "Link kopieren",
|
||||||
"toGoogleCalendar": "Google Kalender",
|
"toGoogleCalendar": "Google Kalender",
|
||||||
"toMicrosoftCalendar": "Microsoft Kalender",
|
"toMicrosoftCalendar": "Microsoft Kalender",
|
||||||
"toHTWKalendar": "HTWKalender"
|
"toHTWKalendar": "HTWKalender",
|
||||||
|
"share": "Teilen",
|
||||||
|
"shareTitle": "HTWKalender",
|
||||||
|
"shareText": "Ich habe diesen Stundenplan für dich mit dem HTWKalender erstellt. Schau ihn dir hier an: ",
|
||||||
|
"shareToastSummary": "Information",
|
||||||
|
"shareToastNotification": "Link zum Stundenplan erfolgreich geteilt",
|
||||||
|
"shareToastError": "Fehler",
|
||||||
|
"shareToastErrorDetail": "Link konnte nicht geteilt werden. Möglicherweise wird die Funktion nicht unterstützt."
|
||||||
},
|
},
|
||||||
"calendarPreview": {
|
"calendarPreview": {
|
||||||
"preview": "Vorschau",
|
"preview": "Vorschau",
|
||||||
@ -157,6 +164,12 @@
|
|||||||
"module": "Modul",
|
"module": "Modul",
|
||||||
"course": "Gruppe"
|
"course": "Gruppe"
|
||||||
},
|
},
|
||||||
|
"calendarViewer": {
|
||||||
|
"location": "Ort",
|
||||||
|
"start": "Beginn",
|
||||||
|
"end": "Ende",
|
||||||
|
"notes": "Notizen"
|
||||||
|
},
|
||||||
"faqView": {
|
"faqView": {
|
||||||
"headline": "Fragen und Antworten",
|
"headline": "Fragen und Antworten",
|
||||||
"firstQuestion": "Wie funktioniert das Kalender erstellen mit dem HTWKalender?",
|
"firstQuestion": "Wie funktioniert das Kalender erstellen mit dem HTWKalender?",
|
||||||
@ -255,13 +268,16 @@
|
|||||||
"headline": "Dein Kalender",
|
"headline": "Dein Kalender",
|
||||||
"subTitle": "Hier findest du die Kalenderansicht von deinem persönlichen Feed.",
|
"subTitle": "Hier findest du die Kalenderansicht von deinem persönlichen Feed.",
|
||||||
"searchPlaceholder": "Token",
|
"searchPlaceholder": "Token",
|
||||||
"searchButton": "Kalender laden"
|
"searchButton": "Kalender laden",
|
||||||
|
"invalidToken": "Ungültiger Token"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"headline": "Einstellungen",
|
"headline": "Einstellungen",
|
||||||
"subTitle": "Hier kannst du deine Einstellungen bearbeiten.",
|
"subTitle": "Hier kannst du deine Einstellungen bearbeiten.",
|
||||||
"language": "Sprache einstellen",
|
"language": "Sprache einstellen",
|
||||||
"darkMode": "Design auswählen",
|
"darkMode": "Design auswählen",
|
||||||
"defaultPage": "Standardseite"
|
"defaultPage": "Standardseite",
|
||||||
|
"version": "App-Version",
|
||||||
|
"reloadPwa": "App neu laden"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,8 +97,13 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"successDetail": "calendar successfully deleted",
|
"successDetail": "calendar successfully deleted",
|
||||||
"errorDetail": "calendar could not be deleted",
|
"errorDetail": "calendar could not be deleted",
|
||||||
"successDetailLoad": "calendar successfully loaded"
|
"successDetailLoad": "calendar successfully loaded",
|
||||||
}
|
"info": "info"
|
||||||
|
},
|
||||||
|
"calendarContainsNoModules": "calendar contains no modules",
|
||||||
|
"noCalendarInOfflineMode": "calendar isn't available in offline mode",
|
||||||
|
"calendarLoadedFromOfflineMode": "calendar loaded from offline cache",
|
||||||
|
"calendarLoadedFromServer": "calendar loaded from web"
|
||||||
},
|
},
|
||||||
"additionalModules": {
|
"additionalModules": {
|
||||||
"subTitle": "select additional modules that are not listed in the regular semester for your course",
|
"subTitle": "select additional modules that are not listed in the regular semester for your course",
|
||||||
@ -149,7 +154,14 @@
|
|||||||
"copyToClipboard": "copy to clipboard",
|
"copyToClipboard": "copy to clipboard",
|
||||||
"toGoogleCalendar": "Google Calendar",
|
"toGoogleCalendar": "Google Calendar",
|
||||||
"toMicrosoftCalendar": "Microsoft Calendar",
|
"toMicrosoftCalendar": "Microsoft Calendar",
|
||||||
"toHTWKalendar": "HTWKalender"
|
"toHTWKalendar": "HTWKalender",
|
||||||
|
"share": "Share",
|
||||||
|
"shareTitle": "HTWKalender",
|
||||||
|
"shareText": "I’ve created this timetable for you with HTWKalender. Check it out here: ",
|
||||||
|
"shareToastSummary": "Information",
|
||||||
|
"shareToastNotification": "Timetable link successfully shared",
|
||||||
|
"shareToastError": "Error",
|
||||||
|
"shareToastErrorDetail": "Link could not be shared. The feature might not be supported."
|
||||||
},
|
},
|
||||||
"calendarPreview": {
|
"calendarPreview": {
|
||||||
"preview": "preview",
|
"preview": "preview",
|
||||||
@ -157,6 +169,12 @@
|
|||||||
"module": "module",
|
"module": "module",
|
||||||
"course": "course"
|
"course": "course"
|
||||||
},
|
},
|
||||||
|
"calendarViewer": {
|
||||||
|
"location": "location",
|
||||||
|
"start": "start",
|
||||||
|
"end": "end",
|
||||||
|
"notes": "notes"
|
||||||
|
},
|
||||||
"faqView": {
|
"faqView": {
|
||||||
"headline": "faq",
|
"headline": "faq",
|
||||||
"firstQuestion": "How does calendar creation work with HTWKalender?",
|
"firstQuestion": "How does calendar creation work with HTWKalender?",
|
||||||
@ -255,13 +273,16 @@
|
|||||||
"headline": "user calendar",
|
"headline": "user calendar",
|
||||||
"subTitle": "Here you can find the calendar view of your personal feed.",
|
"subTitle": "Here you can find the calendar view of your personal feed.",
|
||||||
"searchPlaceholder": "calendar token",
|
"searchPlaceholder": "calendar token",
|
||||||
"searchButton": "load calendar"
|
"searchButton": "load calendar",
|
||||||
|
"invalidToken": "invalid token"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"headline": "Settings",
|
"headline": "Settings",
|
||||||
"subTitle": "Here you can change your settings.",
|
"subTitle": "Here you can change your settings.",
|
||||||
"language": "Choose your language",
|
"language": "Choose your language",
|
||||||
"darkMode": "Switch page theme",
|
"darkMode": "Switch page theme",
|
||||||
"defaultPage": "Default page"
|
"defaultPage": "Default page",
|
||||||
|
"version": "Version",
|
||||||
|
"reloadPwa": "Reload PWA"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,14 @@
|
|||||||
"copyToClipboard": "クリップボードにコピー",
|
"copyToClipboard": "クリップボードにコピー",
|
||||||
"toGoogleCalendar": "Googleカレンダー",
|
"toGoogleCalendar": "Googleカレンダー",
|
||||||
"toMicrosoftCalendar": "Microsoftカレンダー",
|
"toMicrosoftCalendar": "Microsoftカレンダー",
|
||||||
"toHTWKalendar": "HTWカレンダー"
|
"toHTWKalendar": "HTWカレンダー",
|
||||||
|
"share": "共有",
|
||||||
|
"shareTitle": "HTWカレンダー",
|
||||||
|
"shareText": "HTWカレンダーでこの時間割を作成しました。こちらで確認してください: ",
|
||||||
|
"shareToastSummary": "情報",
|
||||||
|
"shareToastNotification": "時間割のリンクが正常に共有されました",
|
||||||
|
"shareToastError": "エラー",
|
||||||
|
"shareToastErrorDetail": "リンクを共有できませんでした。機能がサポートされていない可能性があります。"
|
||||||
},
|
},
|
||||||
"calendarPreview": {
|
"calendarPreview": {
|
||||||
"preview": "プレビュー",
|
"preview": "プレビュー",
|
||||||
@ -157,6 +164,12 @@
|
|||||||
"module": "モジュール",
|
"module": "モジュール",
|
||||||
"course": "コース"
|
"course": "コース"
|
||||||
},
|
},
|
||||||
|
"calendarViewer": {
|
||||||
|
"location": "場所",
|
||||||
|
"start": "開始",
|
||||||
|
"end": "終了",
|
||||||
|
"notes": "メモ"
|
||||||
|
},
|
||||||
"faqView": {
|
"faqView": {
|
||||||
"headline": "よくある質問",
|
"headline": "よくある質問",
|
||||||
"firstQuestion": "HTWカレンダーを使用してカレンダーを作成するにはどうすればよいですか?",
|
"firstQuestion": "HTWカレンダーを使用してカレンダーを作成するにはどうすればよいですか?",
|
||||||
@ -255,13 +268,16 @@
|
|||||||
"headline": "ユーザーカレンダー",
|
"headline": "ユーザーカレンダー",
|
||||||
"subTitle": "ここでは、個人のフィードのカレンダー表示を見つけることができます。",
|
"subTitle": "ここでは、個人のフィードのカレンダー表示を見つけることができます。",
|
||||||
"searchPlaceholder": "カレンダートークン",
|
"searchPlaceholder": "カレンダートークン",
|
||||||
"searchButton": "ロードカレンダー"
|
"searchButton": "ロードカレンダー",
|
||||||
|
"invalidToken": "無効なトークン"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"headline": "設定",
|
"headline": "設定",
|
||||||
"subTitle": "ここで設定を編集できます。",
|
"subTitle": "ここで設定を編集できます。",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
"darkMode": "ダークモード",
|
"darkMode": "ダークモード",
|
||||||
"defaultPage": "デフォルトページ"
|
"defaultPage": "デフォルトページ",
|
||||||
|
"version": "バージョン",
|
||||||
|
"reloadPwa": "PWAを再読み込み"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import App from "./App.vue";
|
|||||||
import PrimeVue from "primevue/config";
|
import PrimeVue from "primevue/config";
|
||||||
import Badge from "primevue/badge";
|
import Badge from "primevue/badge";
|
||||||
import Button from "primevue/button";
|
import Button from "primevue/button";
|
||||||
|
import ButtonGroup from "primevue/buttongroup";
|
||||||
import Dropdown from "primevue/dropdown";
|
import Dropdown from "primevue/dropdown";
|
||||||
import Menu from "primevue/menu";
|
import Menu from "primevue/menu";
|
||||||
import Menubar from "primevue/menubar";
|
import Menubar from "primevue/menubar";
|
||||||
@ -85,6 +86,7 @@ i18n.setup();
|
|||||||
app.use(i18n.vueI18n);
|
app.use(i18n.vueI18n);
|
||||||
app.component("Badge", Badge);
|
app.component("Badge", Badge);
|
||||||
app.component("Button", Button);
|
app.component("Button", Button);
|
||||||
|
app.component("ButtonGroup", ButtonGroup);
|
||||||
app.component("Menu", Menu);
|
app.component("Menu", Menu);
|
||||||
app.component("Menubar", Menubar);
|
app.component("Menubar", Menubar);
|
||||||
app.component("Dialog", Dialog);
|
app.component("Dialog", Dialog);
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
//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/>.
|
||||||
|
|
||||||
import {Binary, Document} from "bson";
|
import { Binary, Document } from "bson";
|
||||||
import { AnonymizedOccupancy } from "./event";
|
import { AnonymizedOccupancy } from "./event";
|
||||||
import {
|
import {
|
||||||
Duration,
|
Duration,
|
||||||
@ -219,31 +219,38 @@ export class RoomOccupancyList {
|
|||||||
// Iterate over all bits in the current byte
|
// Iterate over all bits in the current byte
|
||||||
for (let bit_i = 0; bit_i < 8; bit_i++) {
|
for (let bit_i = 0; bit_i < 8; bit_i++) {
|
||||||
const isOccupied = (byte & (1 << (7 - bit_i))) !== 0;
|
const isOccupied = (byte & (1 << (7 - bit_i))) !== 0;
|
||||||
const calculateOccupancyBitIndex = (byte_i: number, bit_i: number) => byte_i * 8 + bit_i;
|
const calculateOccupancyBitIndex = (byte_i: number, bit_i: number) =>
|
||||||
|
byte_i * 8 + bit_i;
|
||||||
|
|
||||||
if(firstOccupancyBit === null){
|
if (firstOccupancyBit === null) {
|
||||||
if (isOccupied) {
|
if (isOccupied) {
|
||||||
firstOccupancyBit = calculateOccupancyBitIndex(byte_i, bit_i);
|
firstOccupancyBit = calculateOccupancyBitIndex(byte_i, bit_i);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!isOccupied) {
|
if (!isOccupied) {
|
||||||
const startTime = addMinutes(start, firstOccupancyBit * granularity);
|
const startTime = addMinutes(
|
||||||
const endTime = addMinutes(start, calculateOccupancyBitIndex(byte_i, bit_i) * granularity);
|
start,
|
||||||
|
firstOccupancyBit * granularity,
|
||||||
|
);
|
||||||
|
const endTime = addMinutes(
|
||||||
|
start,
|
||||||
|
calculateOccupancyBitIndex(byte_i, bit_i) * granularity,
|
||||||
|
);
|
||||||
// add event between start and end of a block of boolean true values
|
// add event between start and end of a block of boolean true values
|
||||||
occupancyList.push(
|
occupancyList.push(
|
||||||
new AnonymizedOccupancy(
|
new AnonymizedOccupancy(
|
||||||
startTime.toISOString(),
|
startTime.toISOString(),
|
||||||
endTime.toISOString(),
|
endTime.toISOString(),
|
||||||
room,
|
room,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
firstOccupancyBit = null;
|
firstOccupancyBit = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// add last event if it is still ongoing
|
// add last event if it is still ongoing
|
||||||
if (firstOccupancyBit !== null) {
|
if (firstOccupancyBit !== null) {
|
||||||
const startTime = addMinutes(start, firstOccupancyBit * granularity);
|
const startTime = addMinutes(start, firstOccupancyBit * granularity);
|
||||||
@ -317,7 +324,8 @@ export class RoomOccupancyList {
|
|||||||
json.granularity,
|
json.granularity,
|
||||||
json.blocks,
|
json.blocks,
|
||||||
json.rooms.map(
|
json.rooms.map(
|
||||||
(room: { name: string, occupancy: Binary }) => new RoomOccupancy(room.name, room.occupancy),
|
(room: { name: string; occupancy: Binary }) =>
|
||||||
|
new RoomOccupancy(room.name, room.occupancy),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,20 @@
|
|||||||
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
//along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
|
import { Calendar } from "../model/calendar";
|
||||||
|
|
||||||
const tokenStore = defineStore("tokenStore", {
|
const tokenStore = defineStore("tokenStore", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
token: "",
|
token: "",
|
||||||
|
feed: useLocalStorage("feed", {
|
||||||
|
collectionId: "",
|
||||||
|
collectionName: "",
|
||||||
|
created: "",
|
||||||
|
id: "",
|
||||||
|
modules: [],
|
||||||
|
updated: "",
|
||||||
|
} as Calendar),
|
||||||
}),
|
}),
|
||||||
persist: true,
|
persist: true,
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -50,22 +50,66 @@ function loadCalendar(): void {
|
|||||||
moduleStore().removeAllModules();
|
moduleStore().removeAllModules();
|
||||||
tokenStore().setToken(token.value);
|
tokenStore().setToken(token.value);
|
||||||
|
|
||||||
getCalender(token.value).then((data: Module[]) => {
|
if (navigator.onLine) {
|
||||||
if (data.length > 0) {
|
getCalender(token.value).then((data: Module[]) => {
|
||||||
data.forEach((module) => {
|
if (data.length > 0) {
|
||||||
moduleStore().addModule(module);
|
data.forEach((module) => {
|
||||||
});
|
moduleStore().addModule(module);
|
||||||
modules.value = moduleStore().modules;
|
});
|
||||||
router.push("/edit-calendar");
|
modules.value = moduleStore().modules;
|
||||||
|
toast.add({
|
||||||
|
severity: "success",
|
||||||
|
summary: t("editCalendarView.toast.success"),
|
||||||
|
detail: t("editCalendarView.calendarLoadedFromServer"),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push("/edit-calendar");
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: t("editCalendarView.toast.error"),
|
||||||
|
detail: t("editCalendarView.noCalendarFound"),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (tokenStore().feed.id === token.value) {
|
||||||
|
// get data from tokenStore feed if offline
|
||||||
|
const offlineModules = tokenStore().feed.modules;
|
||||||
|
|
||||||
|
if (offlineModules.length > 0) {
|
||||||
|
offlineModules.forEach((module) => {
|
||||||
|
moduleStore().addModule(module);
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: "info",
|
||||||
|
summary: t("editCalendarView.toast.info"),
|
||||||
|
detail: t("editCalendarView.calendarLoadedFromOfflineMode"),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: "info",
|
||||||
|
summary: t("editCalendarView.info"),
|
||||||
|
detail: t("editCalendarView.calendarContainsNoModules"),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
summary: t("editCalendarView.error"),
|
summary: t("editCalendarView.toast.error"),
|
||||||
detail: t("editCalendarView.noCalendarFound"),
|
detail: t("editCalendarView.noCalendarInOfflineMode"),
|
||||||
life: 3000,
|
life: 3000,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
modules.value = moduleStore().modules;
|
||||||
|
router.push("/edit-calendar");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ import LocaleSwitcher from "@/components/LocaleSwitcher.vue";
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import DarkModeSwitcher from "@/components/DarkModeSwitcher.vue";
|
import DarkModeSwitcher from "@/components/DarkModeSwitcher.vue";
|
||||||
import DefaultPageSwitcher from "@/components/DefaultPageSwitcher.vue";
|
import DefaultPageSwitcher from "@/components/DefaultPageSwitcher.vue";
|
||||||
|
import AppVersion from "@/components/AppVersion.vue";
|
||||||
|
import ReloadPwa from "@/components/ReloadPwa.vue";
|
||||||
|
|
||||||
const icon = "pi pi-cog";
|
const icon = "pi pi-cog";
|
||||||
const isDark = ref(true);
|
const isDark = ref(true);
|
||||||
@ -61,11 +63,26 @@ function handleDarkModeToggled(isDarkVar: boolean) {
|
|||||||
></DarkModeSwitcher>
|
></DarkModeSwitcher>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid my-2">
|
||||||
|
<div class="col text-left">
|
||||||
|
{{ $t("settings.version") }}
|
||||||
|
</div>
|
||||||
|
<div class="col text-center">
|
||||||
|
<AppVersion />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid my-2">
|
||||||
|
<div class="col text-center"></div>
|
||||||
|
<div class="col text-center">
|
||||||
|
<ReloadPwa />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
|
||||||
|
<style lang="css" scoped>
|
||||||
.col {
|
.col {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -3,15 +3,17 @@ import CalendarViewer from "@/components/CalendarViewer.vue";
|
|||||||
import DynamicPage from "@/view/DynamicPage.vue";
|
import DynamicPage from "@/view/DynamicPage.vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { extractToken } from "@/helpers/token.ts";
|
import { extractToken, isToken } from "@/helpers/token.ts";
|
||||||
import { useToast } from "primevue/usetoast";
|
import { useToast } from "primevue/usetoast";
|
||||||
import moduleStore from "@/store/moduleStore.ts";
|
import moduleStore from "@/store/moduleStore.ts";
|
||||||
import tokenStore from "@/store/tokenStore.ts";
|
import tokenStore from "@/store/tokenStore.ts";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: "global" });
|
const { t } = useI18n({ useScope: "global" });
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const token = ref(tokenStore().token || ("" as string));
|
const token = ref(tokenStore().token || ("" as string));
|
||||||
|
const calendarViewerRef = ref<InstanceType<typeof CalendarViewer>>();
|
||||||
|
|
||||||
// parse token from query parameter
|
// parse token from query parameter
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@ -22,8 +24,6 @@ if (tokenFromUrl) {
|
|||||||
loadCalendar();
|
loadCalendar();
|
||||||
}
|
}
|
||||||
|
|
||||||
const calendarViewerRef = ref<InstanceType<typeof CalendarViewer>>();
|
|
||||||
|
|
||||||
function loadCalendar() {
|
function loadCalendar() {
|
||||||
try {
|
try {
|
||||||
token.value = extractToken(token.value);
|
token.value = extractToken(token.value);
|
||||||
@ -50,6 +50,35 @@ function loadCalendar() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shareLink() {
|
||||||
|
let datePart = router.currentRoute.value.query.date;
|
||||||
|
if (datePart != undefined) {
|
||||||
|
datePart = "&date=" + datePart;
|
||||||
|
} else {
|
||||||
|
datePart = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = "https://" + window.location.hostname + "/calendar/view?token=" + token.value + datePart;
|
||||||
|
const shareData = {
|
||||||
|
title: t("calendarLink.shareLinkTitle"),
|
||||||
|
text: t("calendarLink.shareLinkText"),
|
||||||
|
url: link,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof navigator.share === "function" && navigator.canShare(shareData)) {
|
||||||
|
navigator.share(shareData);
|
||||||
|
} else {
|
||||||
|
navigator.clipboard.writeText(link).then(() => {
|
||||||
|
toast.add({
|
||||||
|
severity: "info",
|
||||||
|
summary: t("calendarLink.copyToastSummary"),
|
||||||
|
detail: t("calendarLink.copyToastNotification"),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (token.value && token.value !== "") {
|
if (token.value && token.value !== "") {
|
||||||
//loadCalendar();
|
//loadCalendar();
|
||||||
@ -70,11 +99,23 @@ onMounted(() => {
|
|||||||
:class="flexSpecs"
|
:class="flexSpecs"
|
||||||
@keyup.enter="loadCalendar()"
|
@keyup.enter="loadCalendar()"
|
||||||
/>
|
/>
|
||||||
<Button
|
<ButtonGroup
|
||||||
:label="$t('userCalender.searchButton')"
|
:class="flexSpecs"
|
||||||
icon="pi pi-refresh"
|
class="flex no-wrap"
|
||||||
@click="loadCalendar()"
|
:style="{ 'justify-content': 'space-between' }"
|
||||||
></Button>
|
>
|
||||||
|
<Button
|
||||||
|
:label="$t('userCalender.searchButton')"
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
class="flex-grow-1"
|
||||||
|
@click="loadCalendar()"
|
||||||
|
></Button>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-share-alt"
|
||||||
|
:disabled="!isToken(token)"
|
||||||
|
@click="shareLink()"
|
||||||
|
></Button>
|
||||||
|
</ButtonGroup>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<CalendarViewer ref="calendarViewerRef" :token="tokenStore().token" />
|
<CalendarViewer ref="calendarViewerRef" :token="tokenStore().token" />
|
||||||
|
2
frontend/src/vite-env.d.ts
vendored
2
frontend/src/vite-env.d.ts
vendored
@ -17,3 +17,5 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare module "ical.js";
|
declare module "ical.js";
|
||||||
|
|
||||||
|
declare const __APP_VERSION__: string;
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["node"],
|
"types": ["node", "vite-plugin-pwa/vue"],
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
|
@ -28,7 +28,13 @@ export default defineConfig({
|
|||||||
mode: "development",
|
mode: "development",
|
||||||
base: "/",
|
base: "/",
|
||||||
injectRegister: "auto",
|
injectRegister: "auto",
|
||||||
includeAssets: ["favicon.ico", "apple-touch-icon.png", "mask-icon.svg"],
|
includeAssets: [
|
||||||
|
"favicon.ico",
|
||||||
|
"apple-touch-icon.png",
|
||||||
|
"mask-icon.svg",
|
||||||
|
"robots.txt",
|
||||||
|
"sitemap.xml",
|
||||||
|
],
|
||||||
manifest: {
|
manifest: {
|
||||||
name: "HTWKalender",
|
name: "HTWKalender",
|
||||||
short_name: "HTWKalender",
|
short_name: "HTWKalender",
|
||||||
@ -81,55 +87,15 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
registerType: "autoUpdate",
|
registerType: "autoUpdate",
|
||||||
workbox: {
|
strategies: "injectManifest",
|
||||||
|
injectManifest: {
|
||||||
globPatterns: ["**/*.{js,css,html,ico,png,svg,json,vue,txt,woff2}", "/api/schedule/rooms"],
|
globPatterns: ["**/*.{js,css,html,ico,png,svg,json,vue,txt,woff2}", "/api/schedule/rooms"],
|
||||||
cleanupOutdatedCaches: true,
|
|
||||||
runtimeCaching: [
|
|
||||||
{
|
|
||||||
urlPattern: ({ url }) => url.pathname.startsWith("/api/feed"),
|
|
||||||
method: "GET",
|
|
||||||
handler: "NetworkFirst",
|
|
||||||
options: {
|
|
||||||
cacheName: "calendar-feed-cache",
|
|
||||||
expiration: {
|
|
||||||
maxAgeSeconds: 12 * 60 * 60, // 12 hours
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urlPattern: ("/api/schedule/rooms"),
|
|
||||||
method: "GET",
|
|
||||||
handler: "StaleWhileRevalidate",
|
|
||||||
options: {
|
|
||||||
cacheName: "room-schedule-cache",
|
|
||||||
expiration: {
|
|
||||||
maxAgeSeconds: 12 * 60 * 60, // 12 hours
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: {
|
|
||||||
enabled: true,
|
|
||||||
/* when using generateSW the PWA plugin will switch to classic */
|
|
||||||
type: "module",
|
|
||||||
navigateFallback: "index.html",
|
|
||||||
suppressWarnings: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
@ -149,6 +115,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
esbuild: {
|
esbuild: {
|
||||||
supported: {
|
supported: {
|
||||||
"top-level-await": true,
|
"top-level-await": true,
|
||||||
|
Reference in New Issue
Block a user