From b848d26704430313300d2df88677bdc3bc8f133d Mon Sep 17 00:00:00 2001 From: Elmar Kresse Date: Tue, 13 Aug 2024 19:16:07 +0200 Subject: [PATCH] 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",