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",