Skip to content

Commit

Permalink
[Cal-13] Calendar Frontend (#472)
Browse files Browse the repository at this point in the history
* basic impementation of a calendar

* refactored the design to make it more like the google calendar

* Adapted the calendar to changing requirements

* tmp

* made sure that `should_refresh_token()` is subtraction less

* made sure that overfolws don't have ugly scrollbars

* implemented a fully working calendar

* made sure that some icons are more appropriately sized

* renamed
  • Loading branch information
CommanderStorm authored Jul 1, 2024
1 parent 4c2e388 commit 279bf1e
Show file tree
Hide file tree
Showing 18 changed files with 532 additions and 58 deletions.
20 changes: 13 additions & 7 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2099,18 +2099,23 @@ components:
format: int32
examples:
- 42
title:
description: The title of the Entry
stp_title_de:
description: The german title of the Entry
type: string
examples:
- Quantenteleportation
start:
stp_title_en:
description: The english title of the Entry
type: string
examples:
- Quantenteleportation
start_at:
description: The start of the entry
type: string
format: date-time
examples:
- '2018-01-01T00:00:00'
end:
end_at:
description: The end of the entry
type: string
format: date-time
Expand All @@ -2132,9 +2137,10 @@ components:
- Vorlesung mit Zentralübung
required:
- id
- title
- start
- end
- stp_title_de
- stp_title_en
- start_at
- end_at
- entry_type
- detailed_entry_type
DataSources:
Expand Down
3 changes: 2 additions & 1 deletion server/main-api/src/calendar/connectum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ impl APIRequestor {
fn should_refresh_token(&self) -> bool {
if let Some((start, token)) = &self.oauth_token {
if let Some(expires_in) = token.expires_in() {
return expires_in - start.elapsed() < Duration::from_secs(60);
return (expires_in < start.elapsed())
|| (expires_in - start.elapsed() < Duration::from_secs(60));
}
}
true
Expand Down
10 changes: 6 additions & 4 deletions webclient/api_types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,18 +531,20 @@ export type components = {
* @description The id of the calendar entry used in TUMonline internally
*/
readonly id: number;
/** @description The title of the Entry */
readonly title: string;
/** @description The german title of the Entry */
readonly stp_title_de: string;
/** @description The english title of the Entry */
readonly stp_title_en: string;
/**
* Format: date-time
* @description The start of the entry
*/
readonly start: string;
readonly start_at: string;
/**
* Format: date-time
* @description The end of the entry
*/
readonly end: string;
readonly end_at: string;
/**
* @description What this calendar entry means. Each of these should be displayed in a different color
* @enum {string}
Expand Down
100 changes: 100 additions & 0 deletions webclient/components/CalendarFull.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script setup lang="ts">
import FullCalendar from "@fullcalendar/vue3";
import type { CalendarOptions, EventInput, EventSourceFuncArg } from "@fullcalendar/core";
import dayGridPlugin from "@fullcalendar/daygrid";
import type { components, operations } from "~/api_types";
import deLocale from "@fullcalendar/core/locales/de";
import enLocale from "@fullcalendar/core/locales/en-gb";
type CalendarResponse = components["schemas"]["CalendarResponse"];
type CalendarBody = operations["calendar"]["requestBody"]["content"]["application/json"];
type CalendarLocation = components["schemas"]["CalendarLocation"];
const props = defineProps<{ showing: readonly string[] }>();
const runtimeConfig = useRuntimeConfig();
const { locale } = useI18n({ useScope: "local" });
const earliest_last_sync = defineModel<string | null>("earliest_last_sync");
const locations = defineModel<Map<string, CalendarLocation>>("locations");
async function fetchEvents(arg: EventSourceFuncArg): Promise<EventInput[]> {
const body: CalendarBody = {
start_after: arg.startStr,
end_before: arg.endStr,
ids: props.showing,
};
const url = `${runtimeConfig.public.apiURL}/api/calendar`;
const data = await $fetch<CalendarResponse>(url, {
method: "POST",
body: JSON.stringify(body),
retry: 120,
retryDelay: 5000,
});
extractInfos(data);
const items = [];
const show_room_names = Object.keys(data).length > 1;
for (const [k, v] of Object.entries(data)) {
items.push(
...v.events.map((e) => {
const title = locale.value == "de" ? e.stp_title_de : e.stp_title_en;
return {
id: e.id.toString(),
title: show_room_names ? `${k} ${title}` : title,
start: new Date(e.start_at),
end: new Date(e.end_at),
classes: [e.entry_type],
};
}),
);
}
return items;
}
function extractInfos(data: CalendarResponse): void {
earliest_last_sync.value = Object.values(data)
.map((d) => new Date(d.location.last_calendar_scrape_at))
.reduce((d1, d2) => (d1 < d2 ? d1 : d2))
.toLocaleString(locale.value, { timeStyle: "short", dateStyle: "short" });
const tempLocationMap = new Map<string, CalendarLocation>();
for (const [key, v] of Object.entries(data)) {
tempLocationMap.set(key, v.location);
}
locations.value = tempLocationMap;
}
const calendarOptions: CalendarOptions = {
plugins: [dayGridPlugin],
initialView: "dayGridWeek",
weekends: false,
events: fetchEvents,
headerToolbar: {
left: "prev,next",
center: "title",
right: "dayGridMonth,dayGridWeek,dayGridDay",
},
locale: locale.value == "de" ? deLocale : enLocale,
height: 700,
eventTimeFormat: {
// like '14:30'
hour: "2-digit",
minute: "2-digit",
meridiem: false,
},
};
</script>

<template>
<div class="flex max-h-[700px] min-h-[700px] grow flex-col">
<FullCalendar :options="calendarOptions">
<template #eventContent="arg">
<b>{{ arg.timeText }}</b>
<i class="ps-1">{{ arg.event.title }}</i>
</template>
</FullCalendar>
</div>
</template>

<style lang="postcss" scoped>
.fc-daygrid-event-harness {
@apply overflow-x-auto;
}
</style>
140 changes: 140 additions & 0 deletions webclient/components/CalendarModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<script setup lang="ts">
import { useFeedback } from "~/composables/feedback";
import { useCalendar } from "~/composables/calendar";
import type { components } from "~/api_types";
type CalendarLocation = components["schemas"]["CalendarLocation"];
const feedback = useFeedback();
const calendar = useCalendar();
const { t } = useI18n({ useScope: "local" });
// all the below are updated by the calendar
const earliest_last_sync = ref<string | null>(null);
const locations = ref<Map<string, CalendarLocation>>(new Map());
</script>

<template>
<LazyModal v-model="calendar.open" :title="t('title')" class="!min-w-[90vw]">
<NuxtErrorBoundary>
<template #error="{ error }">
<Toast level="error">
<p class="text-md font-bold">{{ t("error.header") }}</p>
<p class="text-sm">
{{ t("error.reason") }}:<br />
<code
class="text-red-900 bg-red-200 mb-1 mt-2 inline-flex max-w-full items-center space-x-2 overflow-auto rounded-md px-4 py-3 text-left font-mono text-xs dark:bg-red-50/20"
>
{{ error }}
</code>
</p>
<I18nT class="text-sm" tag="p" keypath="error.call_to_action">
<template #feedbackForm>
<button
type="button"
class="text-blue-600 bg-transparent visited:text-blue-600 hover:underline"
:aria-label="t('error.feedback-open')"
@click="
() => {
feedback.open = true;
feedback.data = {
category: 'bug',
subject: 'calendar error',
body: `While viewing the calendar for ${JSON.stringify(calendar.showing)}
I got this error:
\`\`\`
${error}
\`\`\`
In case you have trouble replicating this bug, my environment is PLEASE_INSERT_YOUR_BROWSER_HERE.
I also did PLEASE_INSERT_IF_YOU_DID_SOMETHING_SPECIAL_BEFOREHAND`,
deletion_requested: false,
};
}
"
>
{{ t("error.feedback-form") }}
</button>
</template>
</I18nT>
</Toast>
</template>
<template #default>
<div>
<Toast level="info" class="mb-3">
<I18nT class="text-sm" tag="p" keypath="call_for_feedback">
<template #feedbackForm>
<button
type="button"
class="text-blue-600 bg-transparent visited:text-blue-600 hover:underline"
:aria-label="t('error.feedback-open')"
@click="
() => {
feedback.open = true;
feedback.data = {
category: 'general',
subject: 'calendar feedback',
body: `Dear OpenSource@TUM,
The calendar for ${JSON.stringify(calendar.showing)} can be improved by
-
Thanks`,
deletion_requested: false,
};
}
"
>
{{ t("error.feedback-form") }}
</button>
</template>
</I18nT>
</Toast>
<CalendarRoomSelector :data="locations" />
<CalendarFull
v-model:earliest_last_sync="earliest_last_sync"
v-model:locations="locations"
:showing="calendar.showing"
/>
<p class="pt-2 text-xs">
{{ t("footer.disclaimer") }} <br />
{{ t("footer.please_check") }}
<template v-if="earliest_last_sync !== null">
<br />
{{ t("footer.last_sync", [earliest_last_sync]) }}
</template>
</p>
</div>
</template>
</NuxtErrorBoundary>
</LazyModal>
</template>

<i18n lang="yaml">
de:
title: Kalendar
close: Schließen
error:
header: Beim Versuch, den Kalender anzuzeigen, ist ein Fehler aufgetreten
reason: Der Grund für diesen Fehler ist
call_to_action: Wenn dieses Problem weiterhin besteht, kontaktieren Sie uns bitte über das {feedbackForm}.
feedback-form: Feedback-Formular
feedback-open: Feedback-Formular öffnen
footer:
disclaimer: Stündlich aktualisiert und identische Termine zusammengefasst.
please_check: Im Zweifelsfall prüfe bitte den offiziellen TUMonline-Kalender.
last_sync: Stand {0}
call_for_feedback: Diese Funktion ist neu. Wenn du Feedback dazu hast, nutze doch bitte das {feedbackForm}.
en:
title: Calendar
close: Close
error:
header: Got an error trying to display calendar
reason: Reason for this error is
call_to_action: If this issue persists, please contact us via the {feedbackForm}.
feedback-form: feedback form
feedback-open: open the feedback form
footer:
disclaimer: Updated hourly and identical events are merged.
please_check: If in doubt, please check the official calendar on TUMonline
last_sync: Updated {0}
call_for_feedback: This feature is new. If you have some feedback about it, feel free to use the {feedbackForm}.
</i18n>
Loading

0 comments on commit 279bf1e

Please sign in to comment.