From b848d26704430313300d2df88677bdc3bc8f133d Mon Sep 17 00:00:00 2001 From: Elmar Kresse Date: Tue, 13 Aug 2024 19:16:07 +0200 Subject: [PATCH 01/10] feat:#5 added first offline calendar functions --- frontend/src/api/fetchRoomOccupancy.ts | 8 +-- frontend/src/api/loadCalendar.ts | 10 +-- .../src/components/RoomOccupationOffline.vue | 34 +++++----- frontend/src/i18n/translations/en.json | 9 ++- frontend/src/model/roomOccupancyList.ts | 36 ++++++---- frontend/src/store/tokenStore.ts | 10 +++ frontend/src/view/EditCalendarView.vue | 65 ++++++++++++++++--- frontend/src/view/UserCalendar.vue | 3 +- frontend/vite.config.ts | 12 ++++ 9 files changed, 134 insertions(+), 53 deletions(-) diff --git a/frontend/src/api/fetchRoomOccupancy.ts b/frontend/src/api/fetchRoomOccupancy.ts index 59b787b..f067e38 100644 --- a/frontend/src/api/fetchRoomOccupancy.ts +++ b/frontend/src/api/fetchRoomOccupancy.ts @@ -78,10 +78,10 @@ export async function fetchRoomOccupancy( } let roomOccupancyList: RoomOccupancyList = new RoomOccupancyList( - new Date(), - 0, - 0, - [], + new Date(), + 0, + 0, + [], ); await fetch("/api/schedule/rooms?from=" + from_date + "&to=" + to_date) 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/RoomOccupationOffline.vue b/frontend/src/components/RoomOccupationOffline.vue index a9ba72f..b436e62 100644 --- a/frontend/src/components/RoomOccupationOffline.vue +++ b/frontend/src/components/RoomOccupationOffline.vue @@ -21,16 +21,16 @@ import FullCalendar from "@fullcalendar/vue3"; import dayGridPlugin from "@fullcalendar/daygrid"; import interactionPlugin from "@fullcalendar/interaction"; import timeGridPlugin from "@fullcalendar/timegrid"; -import {computed, ComputedRef, inject, ref, Ref, watch} from "vue"; -import {CalendarOptions, DatesSetArg, EventInput} from "@fullcalendar/core"; -import {useI18n} from "vue-i18n"; +import { computed, ComputedRef, inject, ref, Ref, watch } from "vue"; +import { CalendarOptions, DatesSetArg, EventInput } from "@fullcalendar/core"; +import { useI18n } from "vue-i18n"; import allLocales from "@fullcalendar/core/locales-all"; import router from "@/router"; -import {formatYearMonthDay} from "@/helpers/dates"; -import {useQuery} from "@tanstack/vue-query"; -import {fetchRoomOccupancy} from "@/api/fetchRoomOccupancy"; -import {isValid} from "date-fns"; -import {RoomOccupancyList} from "@/model/roomOccupancyList"; +import { formatYearMonthDay } from "@/helpers/dates"; +import { useQuery } from "@tanstack/vue-query"; +import { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy"; +import { isValid } from "date-fns"; +import { RoomOccupancyList } from "@/model/roomOccupancyList"; const { t } = useI18n({ useScope: "global" }); @@ -78,15 +78,15 @@ const selectedRoom = computed(() => props.room); */ function transformData(data: RoomOccupancyList) { return data - .decodeOccupancy( - selectedRoom.value, - new Date(currentDateFrom.value), - new Date(currentDateTo.value), - ) - .map((event, index) => ({ - id: index, - event: event, - })); + .decodeOccupancy( + selectedRoom.value, + new Date(currentDateFrom.value), + new Date(currentDateTo.value), + ) + .map((event, index) => ({ + id: index, + event: event, + })); } const { data: occupancy } = useQuery({ diff --git a/frontend/src/i18n/translations/en.json b/frontend/src/i18n/translations/en.json index 5f24aa8..4ab2c49 100644 --- a/frontend/src/i18n/translations/en.json +++ b/frontend/src/i18n/translations/en.json @@ -95,8 +95,13 @@ "error": "Error", "successDetail": "calendar successfully 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": { "subTitle": "select additional modules that are not listed in the regular semester for your course", diff --git a/frontend/src/model/roomOccupancyList.ts b/frontend/src/model/roomOccupancyList.ts index c58fc90..0d31057 100644 --- a/frontend/src/model/roomOccupancyList.ts +++ b/frontend/src/model/roomOccupancyList.ts @@ -14,7 +14,7 @@ //You should have received a copy of the GNU Affero General Public License //along with this program. If not, see . -import {Binary, Document} from "bson"; +import { Binary, Document } from "bson"; import { AnonymizedOccupancy } from "./event"; import { Duration, @@ -218,31 +218,38 @@ export class RoomOccupancyList { // Iterate over all bits in the current byte for (let bit_i = 0; bit_i < 8; bit_i++) { 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) { firstOccupancyBit = calculateOccupancyBitIndex(byte_i, bit_i); } } else { if (!isOccupied) { - const startTime = addMinutes(start, firstOccupancyBit * granularity); - const endTime = addMinutes(start, calculateOccupancyBitIndex(byte_i, bit_i) * granularity); + const startTime = addMinutes( + 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 occupancyList.push( - new AnonymizedOccupancy( - startTime.toISOString(), - endTime.toISOString(), - room, - false, - false, - ), + new AnonymizedOccupancy( + startTime.toISOString(), + endTime.toISOString(), + room, + false, + false, + ), ); firstOccupancyBit = null; } } - } } + } // add last event if it is still ongoing if (firstOccupancyBit !== null) { const startTime = addMinutes(start, firstOccupancyBit * granularity); @@ -315,7 +322,8 @@ export class RoomOccupancyList { json.granularity, json.blocks, 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), ), ); } diff --git a/frontend/src/store/tokenStore.ts b/frontend/src/store/tokenStore.ts index 2d607b8..f404673 100644 --- a/frontend/src/store/tokenStore.ts +++ b/frontend/src/store/tokenStore.ts @@ -15,10 +15,20 @@ //along with this program. If not, see . import { defineStore } from "pinia"; +import { useLocalStorage } from "@vueuse/core"; +import { Calendar } from "../model/calendar"; const tokenStore = defineStore("tokenStore", { state: () => ({ token: "", + feed: useLocalStorage("feed", { + collectionId: "", + collectionName: "", + created: "", + id: "", + modules: [], + updated: "", + } as Calendar), }), persist: true, actions: { diff --git a/frontend/src/view/EditCalendarView.vue b/frontend/src/view/EditCalendarView.vue index 58d3193..120ab53 100644 --- a/frontend/src/view/EditCalendarView.vue +++ b/frontend/src/view/EditCalendarView.vue @@ -50,22 +50,67 @@ function loadCalendar(): void { moduleStore().removeAllModules(); tokenStore().setToken(token.value); - getCalender(token.value).then((data: Module[]) => { - if (data.length > 0) { - data.forEach((module) => { - moduleStore().addModule(module); - }); - modules.value = moduleStore().modules; - router.push("/edit-calendar"); + if (navigator.onLine) { + getCalender(token.value).then((data: Module[]) => { + if (data.length > 0) { + data.forEach((module) => { + moduleStore().addModule(module); + }); + 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 { toast.add({ severity: "error", - summary: t("editCalendarView.error"), - detail: t("editCalendarView.noCalendarFound"), + summary: t("editCalendarView.toast.error"), + detail: t("editCalendarView.noCalendarInOfflineMode"), life: 3000, }); + return; + } + modules.value = moduleStore().modules; + router.push("/edit-calendar"); } - }); } diff --git a/frontend/src/view/UserCalendar.vue b/frontend/src/view/UserCalendar.vue index 2ec6ae0..856d637 100644 --- a/frontend/src/view/UserCalendar.vue +++ b/frontend/src/view/UserCalendar.vue @@ -12,6 +12,7 @@ const { t } = useI18n({ useScope: "global" }); const toast = useToast(); const token = ref(tokenStore().token || ("" as string)); +const calendarViewerRef = ref>(); // parse token from query parameter const urlParams = new URLSearchParams(window.location.search); @@ -22,8 +23,6 @@ if (tokenFromUrl) { loadCalendar(); } -const calendarViewerRef = ref>(); - function loadCalendar() { try { token.value = extractToken(token.value); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 98fe21b..2eaf19d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -96,6 +96,18 @@ export default defineConfig({ }, }, }, + { + // Add the runtime caching strategy for /api/modules + urlPattern: ({url}) => url.pathname.startsWith('/api/modules'), + method: 'GET', + handler: 'NetworkFirst', + options: { + cacheName: 'modules-cache', + expiration: { + maxAgeSeconds: 24 * 60 * 60, // 1 day + }, + }, + }, { urlPattern: /^https?.*/, handler: "NetworkFirst", From a22c7a107305f143a8721cd21569aca42697e99b Mon Sep 17 00:00:00 2001 From: Elmar Kresse Date: Tue, 13 Aug 2024 20:41:45 +0200 Subject: [PATCH 02/10] feat:#5 added own service worker script for precache api during install --- frontend/package-lock.json | 10 ++++---- frontend/package.json | 4 ++- frontend/public/sw.js | 28 +++++++++++++++++++++ frontend/vite.config.ts | 50 ++++++-------------------------------- 4 files changed, 44 insertions(+), 48 deletions(-) create mode 100644 frontend/public/sw.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e2d2a05..95cfd33 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,7 +31,9 @@ "source-sans": "^3.46.0", "vue": "^3.4.11", "vue-i18n": "^9.13.1", - "vue-router": "^4.3.2" + "vue-router": "^4.3.2", + "workbox-core": "^7.1.0", + "workbox-precaching": "^7.1.0" }, "devDependencies": { "@types/node": "^20.12.12", @@ -8806,7 +8808,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.1.0.tgz", "integrity": "sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==", - "dev": true + "license": "MIT" }, "node_modules/workbox-expiration": { "version": "7.1.0", @@ -8843,7 +8845,7 @@ "version": "7.1.0", "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", @@ -8877,7 +8879,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.1.0.tgz", "integrity": "sha512-oOYk+kLriUY2QyHkIilxUlVcFqwduLJB7oRZIENbqPGeBP/3TWHYNNdmGNhz1dvKuw7aqvJ7CQxn27/jprlTdg==", - "dev": true, "dependencies": { "workbox-core": "7.1.0" } @@ -8886,7 +8887,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.1.0.tgz", "integrity": "sha512-/UracPiGhUNehGjRm/tLUQ+9PtWmCbRufWtV0tNrALuf+HZ4F7cmObSEK+E4/Bx1p8Syx2tM+pkIrvtyetdlew==", - "dev": true, "dependencies": { "workbox-core": "7.1.0" } diff --git a/frontend/package.json b/frontend/package.json index bd35f2f..7b4b4fc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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..a2616c3 --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,28 @@ +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); + }); + }) + ); +}); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 2eaf19d..087a244 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -28,7 +28,13 @@ export default defineConfig({ mode: "development", base: "/", 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: { name: "HTWKalender", short_name: "HTWKalender", @@ -81,47 +87,6 @@ export default defineConfig({ ], }, registerType: "autoUpdate", - workbox: { - globPatterns: ["**/*.{js,css,html,ico,png,svg,json,vue,txt,woff2}"], - 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 - }, - }, - }, - { - // Add the runtime caching strategy for /api/modules - urlPattern: ({url}) => url.pathname.startsWith('/api/modules'), - method: 'GET', - handler: 'NetworkFirst', - options: { - cacheName: 'modules-cache', - expiration: { - maxAgeSeconds: 24 * 60 * 60, // 1 day - }, - }, - }, - { - 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 */ @@ -129,6 +94,7 @@ export default defineConfig({ navigateFallback: "index.html", suppressWarnings: true, }, + strategies: "injectManifest", }), ], resolve: { From ebbfb6b48e343d3abb65a0ec856233f96aee68c4 Mon Sep 17 00:00:00 2001 From: survellow <59056368+survellow@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:53:07 +0200 Subject: [PATCH 03/10] feat:#17 add share button on calendar link page --- frontend/src/components/CalendarLink.vue | 92 ++++++++++++++++++------ frontend/src/i18n/translations/de.json | 9 ++- frontend/src/i18n/translations/en.json | 9 ++- frontend/src/i18n/translations/ja.json | 9 ++- 4 files changed, 94 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/CalendarLink.vue b/frontend/src/components/CalendarLink.vue index cf3d40c..6ab90c4 100644 --- a/frontend/src/components/CalendarLink.vue +++ b/frontend/src/components/CalendarLink.vue @@ -84,28 +84,76 @@ 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 shareLink = () => { + if (typeof navigator.share === 'function' && navigator.canShare()) { + navigator + .share({ + title: t("calendarLink.shareTitle"), + text: t("calendarLink.shareText") + getLink(), + url: "https://" + domain + "/", + }) + .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(() => { + var 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()) { + 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/i18n/index.ts b/frontend/src/i18n/index.ts index 4db4b2f..3531804 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -34,6 +34,56 @@ function setup() { de, ja, }, + datetimeFormats: { + en: { + short: { + year: "numeric", + month: "short", + day: "numeric", + }, + long: { + year: "numeric", + month: "short", + day: "numeric", + weekday: "short", + hour: "numeric", + minute: "numeric", + hour12: true, + }, + }, + de: { + short: { + year: "numeric", + month: "short", + day: "numeric", + }, + long: { + year: "numeric", + month: "short", + day: "numeric", + weekday: "short", + hour: "numeric", + minute: "numeric", + hour12: false, + }, + }, + ja: { + short: { + year: "numeric", + month: "short", + day: "numeric", + }, + long: { + year: "numeric", + month: "short", + day: "numeric", + weekday: "short", + hour: "numeric", + minute: "numeric", + hour12: true, + }, + }, + }, }); return _i18n; } diff --git a/frontend/src/i18n/translations/de.json b/frontend/src/i18n/translations/de.json index 7bc898f..7eff816 100644 --- a/frontend/src/i18n/translations/de.json +++ b/frontend/src/i18n/translations/de.json @@ -162,6 +162,12 @@ "module": "Modul", "course": "Gruppe" }, + "calendarViewer": { + "location": "Ort", + "start": "Beginn", + "end": "Ende", + "notes": "Notizen" + }, "faqView": { "headline": "Fragen und Antworten", "firstQuestion": "Wie funktioniert das Kalender erstellen mit dem HTWKalender?", @@ -260,7 +266,8 @@ "headline": "Dein Kalender", "subTitle": "Hier findest du die Kalenderansicht von deinem persönlichen Feed.", "searchPlaceholder": "Token", - "searchButton": "Kalender laden" + "searchButton": "Kalender laden", + "invalidToken": "Ungültiger Token" }, "settings": { "headline": "Einstellungen", diff --git a/frontend/src/i18n/translations/en.json b/frontend/src/i18n/translations/en.json index 8291ed8..c95f1ba 100644 --- a/frontend/src/i18n/translations/en.json +++ b/frontend/src/i18n/translations/en.json @@ -167,6 +167,12 @@ "module": "module", "course": "course" }, + "calendarViewer": { + "location": "location", + "start": "start", + "end": "end", + "notes": "notes" + }, "faqView": { "headline": "faq", "firstQuestion": "How does calendar creation work with HTWKalender?", @@ -265,7 +271,8 @@ "headline": "user calendar", "subTitle": "Here you can find the calendar view of your personal feed.", "searchPlaceholder": "calendar token", - "searchButton": "load calendar" + "searchButton": "load calendar", + "invalidToken": "invalid token" }, "settings": { "headline": "Settings", diff --git a/frontend/src/i18n/translations/ja.json b/frontend/src/i18n/translations/ja.json index e68714f..bd2ac05 100644 --- a/frontend/src/i18n/translations/ja.json +++ b/frontend/src/i18n/translations/ja.json @@ -162,6 +162,12 @@ "module": "モジュール", "course": "コース" }, + "calendarViewer": { + "location": "場所", + "start": "開始", + "end": "終了", + "notes": "メモ" + }, "faqView": { "headline": "よくある質問", "firstQuestion": "HTWカレンダーを使用してカレンダーを作成するにはどうすればよいですか?", @@ -260,7 +266,8 @@ "headline": "ユーザーカレンダー", "subTitle": "ここでは、個人のフィードのカレンダー表示を見つけることができます。", "searchPlaceholder": "カレンダートークン", - "searchButton": "ロードカレンダー" + "searchButton": "ロードカレンダー", + "invalidToken": "無効なトークン" }, "settings": { "headline": "設定", diff --git a/frontend/src/main.ts b/frontend/src/main.ts index a22c319..3b18df9 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -22,6 +22,7 @@ import App from "./App.vue"; import PrimeVue from "primevue/config"; import Badge from "primevue/badge"; import Button from "primevue/button"; +import ButtonGroup from "primevue/buttongroup"; import Dropdown from "primevue/dropdown"; import Menu from "primevue/menu"; import Menubar from "primevue/menubar"; @@ -85,6 +86,7 @@ i18n.setup(); app.use(i18n.vueI18n); app.component("Badge", Badge); app.component("Button", Button); +app.component("ButtonGroup", ButtonGroup); app.component("Menu", Menu); app.component("Menubar", Menubar); app.component("Dialog", Dialog); diff --git a/frontend/src/view/UserCalendar.vue b/frontend/src/view/UserCalendar.vue index 856d637..1eec673 100644 --- a/frontend/src/view/UserCalendar.vue +++ b/frontend/src/view/UserCalendar.vue @@ -3,10 +3,11 @@ 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 { extractToken, isToken } from "@/helpers/token.ts"; import { useToast } from "primevue/usetoast"; import moduleStore from "@/store/moduleStore.ts"; import tokenStore from "@/store/tokenStore.ts"; +import router from "@/router"; const { t } = useI18n({ useScope: "global" }); const toast = useToast(); @@ -49,6 +50,33 @@ 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; + if (typeof navigator.share === "function" && navigator.canShare()) { + navigator.share({ + title: t("calendarLink.shareLinkTitle"), + text: t("calendarLink.shareLinkText"), + url: link, + }); + } else { + navigator.clipboard.writeText(link).then(() => { + toast.add({ + severity: "info", + summary: t("calendarLink.copyToastSummary"), + detail: t("calendarLink.copyToastNotification"), + life: 3000, + }); + }); + } +} + onMounted(() => { if (token.value && token.value !== "") { //loadCalendar(); @@ -69,11 +97,23 @@ onMounted(() => { :class="flexSpecs" @keyup.enter="loadCalendar()" /> - + + + + From 7825cae8aacbf56b00c00aa92913c2eac93b9b8f Mon Sep 17 00:00:00 2001 From: survellow <59056368+survellow@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:21:07 +0200 Subject: [PATCH 08/10] fix:#17 linter error --- frontend/src/components/CalendarViewer.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/CalendarViewer.vue b/frontend/src/components/CalendarViewer.vue index 19fb504..02fee48 100644 --- a/frontend/src/components/CalendarViewer.vue +++ b/frontend/src/components/CalendarViewer.vue @@ -214,7 +214,7 @@ watch(mobilePage, () => {

{{ $t("calendarViewer.start") }}: {{ clickedEvent.start ? $d(clickedEvent.start, "long") : ""}}

{{ $t("calendarViewer.end") }}: {{ clickedEvent.end ? $d(clickedEvent.end, "long") : "" }}

{{ $t("calendarViewer.notes") }}:

-

{{ note }}

+

{{ note }}

From b72cdd85e195af76026b467c8ca549116ea7d00c Mon Sep 17 00:00:00 2001 From: Elmar Kresse Date: Mon, 9 Sep 2024 15:52:29 +0200 Subject: [PATCH 09/10] added version info and reload option --- frontend/package-lock.json | 70 +++++++++------------- frontend/package.json | 2 +- frontend/public/sw.js | 18 +++--- frontend/src/components/AppVersion.vue | 9 +++ frontend/src/components/LocaleSwitcher.vue | 7 +-- frontend/src/components/ReloadPwa.vue | 39 ++++++++++++ frontend/src/i18n/translations/de.json | 4 +- frontend/src/i18n/translations/en.json | 4 +- frontend/src/i18n/translations/ja.json | 4 +- frontend/src/view/EditCalendarView.vue | 9 ++- frontend/src/view/SettingsView.vue | 19 +++++- frontend/src/vite-env.d.ts | 2 + frontend/tsconfig.json | 2 +- frontend/vite.config.ts | 18 +++--- 14 files changed, 131 insertions(+), 76 deletions(-) create mode 100644 frontend/src/components/AppVersion.vue create mode 100644 frontend/src/components/ReloadPwa.vue diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 95cfd33..b663b4d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,9 +31,7 @@ "source-sans": "^3.46.0", "vue": "^3.4.11", "vue-i18n": "^9.13.1", - "vue-router": "^4.3.2", - "workbox-core": "^7.1.0", - "workbox-precaching": "^7.1.0" + "vue-router": "^4.3.2" }, "devDependencies": { "@types/node": "^20.12.12", @@ -50,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": { @@ -2983,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", @@ -3826,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" @@ -4633,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", @@ -6331,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" @@ -7664,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" @@ -8409,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", @@ -8808,6 +8790,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.1.0.tgz", "integrity": "sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==", + "dev": true, "license": "MIT" }, "node_modules/workbox-expiration": { @@ -8845,6 +8828,7 @@ "version": "7.1.0", "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", @@ -8879,6 +8863,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.1.0.tgz", "integrity": "sha512-oOYk+kLriUY2QyHkIilxUlVcFqwduLJB7oRZIENbqPGeBP/3TWHYNNdmGNhz1dvKuw7aqvJ7CQxn27/jprlTdg==", + "dev": true, "dependencies": { "workbox-core": "7.1.0" } @@ -8887,6 +8872,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.1.0.tgz", "integrity": "sha512-/UracPiGhUNehGjRm/tLUQ+9PtWmCbRufWtV0tNrALuf+HZ4F7cmObSEK+E4/Bx1p8Syx2tM+pkIrvtyetdlew==", + "dev": true, "dependencies": { "workbox-core": "7.1.0" } diff --git a/frontend/package.json b/frontend/package.json index 7b4b4fc..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", diff --git a/frontend/public/sw.js b/frontend/public/sw.js index a2616c3..2533cd7 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,5 +1,5 @@ -import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching'; -import { clientsClaim } from 'workbox-core'; +import { precacheAndRoute, cleanupOutdatedCaches } from "workbox-precaching"; +import { clientsClaim } from "workbox-core"; self.skipWaiting(); clientsClaim(); @@ -10,19 +10,19 @@ cleanupOutdatedCaches(); precacheAndRoute(self.__WB_MANIFEST); // Custom precaching logic for the /api/modules endpoint -self.addEventListener('install', (event) => { +self.addEventListener("install", (event) => { event.waitUntil( - caches.open('api-modules-cache').then((cache) => { - return fetch('/api/modules') + caches.open("api-modules-cache").then((cache) => { + return fetch("/api/modules") .then((response) => { if (response.ok) { - return cache.put('/api/modules', response); + return cache.put("/api/modules", response); } - throw new Error('Failed to fetch /api/modules'); + throw new Error("Failed to fetch /api/modules"); }) .catch((error) => { - console.error('Precaching /api/modules failed:', error); + console.error("Precaching /api/modules failed:", error); }); - }) + }), ); }); 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/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); -