Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cal-13] Calendar Frontend #472

Merged
merged 14 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion server/main-api/src/calendar/connectum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ 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 start.elapsed() < expires_in + Duration::from_secs(60);
CommanderStorm marked this conversation as resolved.
Show resolved Hide resolved
}
}
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