diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e2d2a05..b663b4d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -48,7 +48,9 @@ "vite": "^5.2.11", "vite-plugin-pwa": "^0.20.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": { @@ -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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -3824,11 +3804,12 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, + "license": "MIT", "peer": true, "peerDependencies": { "acorn": "^8" @@ -4631,10 +4612,11 @@ "dev": true }, "node_modules/enhanced-resolve": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", - "integrity": "sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -6329,10 +6311,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -7662,6 +7645,7 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -8407,22 +8391,22 @@ "dev": true }, "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -8806,7 +8790,8 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.1.0.tgz", "integrity": "sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/workbox-expiration": { "version": "7.1.0", @@ -8844,6 +8829,7 @@ "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.1.0.tgz", "integrity": "sha512-LyxzQts+UEpgtmfnolo0hHdNjoB7EoRWcF7EDslt+lQGd0lW4iTvvSe3v5JiIckQSB5KTW5xiCqjFviRKPj1zA==", "dev": true, + "license": "MIT", "dependencies": { "workbox-core": "7.1.0", "workbox-routing": "7.1.0", diff --git a/frontend/package.json b/frontend/package.json index bd35f2f..7d6b5c6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "htwkalender", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", @@ -53,6 +53,8 @@ "vite": "^5.2.11", "vite-plugin-pwa": "^0.20.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" } } diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..878156a --- /dev/null +++ b/frontend/public/sw.js @@ -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; + }); + }), + ); + } +}); diff --git a/frontend/src/api/loadCalendar.ts b/frontend/src/api/loadCalendar.ts index bf76f17..98923e0 100644 --- a/frontend/src/api/loadCalendar.ts +++ b/frontend/src/api/loadCalendar.ts @@ -15,7 +15,8 @@ //along with this program. If not, see . 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 { const request = new Request("/api/collections/feeds/records/" + token, { @@ -24,9 +25,10 @@ export async function getCalender(token: string): Promise { return await fetch(request).then((response) => { if (response.ok) { - return response - .json() - .then((calendarResponse: Calendar) => calendarResponse.modules); + return response.json().then((calendarResponse: Calendar) => { + tokenStore().feed = calendarResponse; + return calendarResponse.modules; + }); } else { return []; } diff --git a/frontend/src/components/AppVersion.vue b/frontend/src/components/AppVersion.vue new file mode 100644 index 0000000..5101f50 --- /dev/null +++ b/frontend/src/components/AppVersion.vue @@ -0,0 +1,9 @@ + + + diff --git a/frontend/src/components/CalendarLink.vue b/frontend/src/components/CalendarLink.vue index cf3d40c..c7ca242 100644 --- a/frontend/src/components/CalendarLink.vue +++ b/frontend/src/components/CalendarLink.vue @@ -84,28 +84,78 @@ const forwardToHTWKalendar = () => { }); }; -const actions = computed(() => [ - { - 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, - }, -]); +const shareData = computed(() => ({ + title: t("calendarLink.shareTitle"), + text: t("calendarLink.shareText") + getLink(), + url: "https://" + domain + "/", +})); + +const shareLink = () => { + if (typeof navigator.share === 'function' && navigator.canShare(shareData.value)) { + navigator + .share(shareData.value) + .then(() => { + toast.add({ + severity: "info", + summary: t("calendarLink.shareToastSummary"), + detail: t("calendarLink.shareToastNotification"), + life: 3000, + }); + }) + .catch(() => { + 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; +} + +); @@ -215,4 +225,9 @@ watch(mobilePage, () => { justify-content: space-between; gap: 0.5rem; } + +.note-line { + margin: 0; + margin-left: 2rem; +} diff --git a/frontend/src/components/LocaleSwitcher.vue b/frontend/src/components/LocaleSwitcher.vue index f274c39..1ec777d 100644 --- a/frontend/src/components/LocaleSwitcher.vue +++ b/frontend/src/components/LocaleSwitcher.vue @@ -68,12 +68,7 @@ updateLocale(settingsStore().locale); -