Implement graphql query for events calendar and agenda components

- remove ICSCalendar and ICSAgenda components
- fix highlight selected date

#40
#41
This commit is contained in:
summersamara 2024-01-04 15:03:40 +01:00
parent dec26525c0
commit a48b315f16
5 changed files with 260 additions and 116 deletions

View file

@ -8,37 +8,22 @@
<div v-if="listOfEventsByDate.date" class="my-4">
<b v-text="formatDateString(listOfEventsByDate.date)" />
<div v-for="(event, index) in listOfEventsByDate.events" v-bind:key="index">
<a :href="event.event.url">
<div
class="text-violet-3 dark:text-white bg-mbz-yellow dark:bg-mbz-purple my-2 p-2 rounded"
>
<div class="mb-1">
<Clock class="clock-icon" />
<b class="ml-1 time">
{{ formatTimeString(event.event.startStr, undefined) }}
{{
event.event.endStr
? "- " + formatTimeString(event.event.endStr, undefined)
: ""
}}
</b>
</div>
<span class="font-bold line-clamp-4">
{{ event.event.title }}
</span>
<div class="mt-1">
<small>
{{
t("Organized by {name}", {
name: event.event.extendedProps.organizer,
})
}}
</small>
</div>
<div v-if="listOfEventsByDate.events.length > 0">
<div
v-for="(event, index) in listOfEventsByDate.events"
v-bind:key="index"
>
<div class="scroll-ml-6 snap-center shrink-0 my-4">
<EventCard :event="event.event.extendedProps.event" />
</div>
</a>
</div>
</div>
<EmptyContent v-else icon="calendar" :inline="true">
<span>
{{ t("No events found") }}
</span>
</EmptyContent>
</div>
</template>
@ -46,7 +31,10 @@
import { useI18n } from "vue-i18n";
import { locale } from "@/utils/i18n";
import { computed, ref } from "vue";
import Clock from "vue-material-design-icons/ClockTimeTenOutline.vue";
import { useLazyQuery } from "@vue/apollo-composable";
import { IEvent } from "@/types/event.model";
import { Paginate } from "@/types/paginate";
import { SEARCH_CALENDAR_EVENTS } from "@/graphql/search";
import FullCalendar from "@fullcalendar/vue3";
import { EventSegment } from "@fullcalendar/core";
import dayGridPlugin from "@fullcalendar/daygrid";
@ -55,38 +43,94 @@ import interactionPlugin from "@fullcalendar/interaction";
import {
formatDateISOStringWithoutTime,
formatDateString,
formatTimeString,
} from "@/filters/datetime";
const props = defineProps<{
icsFeedUrl: string;
}>();
import EventCard from "../Event/EventCard.vue";
import EmptyContent from "../Utils/EmptyContent.vue";
const { t } = useI18n({ useScope: "global" });
const calendarRef = ref();
const lastSelectedDate = ref<string | undefined>(undefined);
const lastSelectedDate = ref<string | undefined>(new Date().toISOString());
const listOfEventsByDate = ref<{ events: EventSegment[]; date?: string }>({
events: [],
date: undefined,
});
const showEventsByDate = (dateStr: string) => {
dateStr = formatDateISOStringWithoutTime(dateStr);
const moreLinkElement = document.querySelectorAll(
`td[data-date='${dateStr}'] a.fc-more-link`
)[0] as undefined | HTMLElement;
if (moreLinkElement) {
moreLinkElement.click();
} else {
listOfEventsByDate.value = {
events: [],
date: dateStr,
};
}
calendarRef.value.getApi().select(dateStr);
};
if (window.location.hash.length) {
lastSelectedDate.value = formatDateISOStringWithoutTime(
window.location.hash.replace("#_", "")
);
} else {
lastSelectedDate.value = formatDateISOStringWithoutTime(
new Date().toISOString()
);
}
const { load: searchEventsLoad, refetch: searchEventsRefetch } = useLazyQuery<{
searchEvents: Paginate<IEvent>;
}>(SEARCH_CALENDAR_EVENTS);
const calendarOptions = computed((): object => {
return {
plugins: [dayGridPlugin, iCalendarPlugin, interactionPlugin],
initialView: "dayGridMonth",
initialDate: lastSelectedDate.value,
events: {
url: props.icsFeedUrl,
format: "ics",
events: async (
info: { start: Date; end: Date; startStr: string; endStr: string },
successCallback: (arg: object[]) => unknown,
failureCallback: (err: string) => unknown
) => {
const queryVars = {
limit: 999,
beginsOn: info.start,
endsOn: info.end,
};
const result =
(await searchEventsLoad(undefined, queryVars)) ||
(await searchEventsRefetch(queryVars))?.data;
if (!result) {
failureCallback("failed to fetch calendar events");
return;
}
successCallback(
(result.searchEvents.elements ?? []).map((event: IEvent) => {
return {
id: event.id,
title: event.title,
start: event.beginsOn,
end: event.endsOn,
startStr: event.beginsOn,
endStr: event.endsOn,
url: event.url,
extendedProps: {
event: event,
},
};
})
);
},
nextDayThreshold: "09:00:00",
dayMaxEventRows: 0,
@ -97,7 +141,7 @@ const calendarOptions = computed((): object => {
contentHeight: "auto",
eventClassNames: "line-clamp-3 bg-mbz-yellow dark:bg-mbz-purple",
headerToolbar: {
left: "prev,next,today",
left: "prev,next,customTodayButton",
center: "",
right: "title",
},
@ -110,16 +154,19 @@ const calendarOptions = computed((): object => {
day: t("Day"),
list: t("List"),
},
dateClick: (info: { dateStr: string }) => {
calendarRef.value.getApi().select(info.dateStr);
customButtons: {
customTodayButton: {
text: t("Today"),
click: () => {
calendarRef.value.getApi().today();
lastSelectedDate.value = formatDateISOStringWithoutTime(
new Date().toISOString()
);
},
},
},
select: (info: { startStr: string }) => {
const startDateStr = formatDateISOStringWithoutTime(info.startStr);
const moreLinkElement = document.querySelectorAll(
`td[data-date='${startDateStr}'] a.fc-more-link`
)[0] as undefined | HTMLElement;
moreLinkElement?.click();
dateClick: (info: { dateStr: string }) => {
showEventsByDate(info.dateStr);
},
moreLinkClick: (info: {
date: Date;
@ -144,7 +191,7 @@ const calendarOptions = computed((): object => {
lastSelectedDate.value &&
arg.el.closest(`td[data-date='${lastSelectedDate.value}']`)
) {
calendarRef.value.getApi().select(lastSelectedDate.value);
showEventsByDate(lastSelectedDate.value);
lastSelectedDate.value = undefined;
}
},
@ -167,6 +214,10 @@ const calendarOptions = computed((): object => {
margin-left: 0.1rem !important;
}
.agenda-view .fc-more-link {
pointer-events: none !important;
}
.clock-icon {
display: inline-block;
vertical-align: middle;

View file

@ -0,0 +1,105 @@
<template>
<FullCalendar ref="calendarRef" :options="calendarOptions">
<template v-slot:eventContent="arg">
<span
class="text-violet-3 dark:text-white font-bold m-2"
:title="arg.event.title"
>
{{ arg.event.title }}
</span>
</template>
</FullCalendar>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { locale } from "@/utils/i18n";
import { computed, ref } from "vue";
import { useLazyQuery } from "@vue/apollo-composable";
import { IEvent } from "@/types/event.model";
import { Paginate } from "@/types/paginate";
import { SEARCH_CALENDAR_EVENTS } from "@/graphql/search";
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import iCalendarPlugin from "@fullcalendar/icalendar";
import interactionPlugin from "@fullcalendar/interaction";
const calendarRef = ref();
const { t } = useI18n({ useScope: "global" });
const { load: searchEventsLoad, refetch: searchEventsRefetch } = useLazyQuery<{
searchEvents: Paginate<IEvent>;
}>(SEARCH_CALENDAR_EVENTS);
const calendarOptions = computed((): object => {
return {
plugins: [dayGridPlugin, iCalendarPlugin, interactionPlugin],
initialView: "dayGridMonth",
events: async (
info: { start: Date; end: Date; startStr: string; endStr: string },
successCallback: (arg: object[]) => unknown,
failureCallback: (err: string) => unknown
) => {
const queryVars = {
limit: 999,
beginsOn: info.start,
endsOn: info.end,
};
const result =
(await searchEventsLoad(undefined, queryVars)) ||
(await searchEventsRefetch(queryVars))?.data;
if (!result) {
failureCallback("failed to fetch calendar events");
return;
}
successCallback(
(result.searchEvents.elements ?? []).map((event: IEvent) => {
return {
id: event.id,
title: event.title,
start: event.beginsOn,
end: event.endsOn,
startStr: event.beginsOn,
endStr: event.endsOn,
url: `/events/${event.uuid}`,
extendedProps: {
event: event,
},
};
})
);
},
nextDayThreshold: "09:00:00",
dayMaxEventRows: 5,
moreLinkClassNames: "bg-mbz-yellow dark:bg-mbz-purple dark:text-white p-2",
moreLinkContent: (arg: { num: number; text: string }) => {
return "+" + arg.num.toString();
},
eventClassNames: "line-clamp-3 bg-mbz-yellow dark:bg-mbz-purple",
headerToolbar: {
left: "prev,next,today",
center: "title",
right: "dayGridWeek,dayGridMonth", // user can switch between the two
},
locale: locale,
firstDay: 1,
buttonText: {
today: t("Today"),
month: t("Month"),
week: t("Week"),
day: t("Day"),
list: t("List"),
},
};
});
</script>
<style>
.fc-popover-header {
color: black !important;
}
</style>

View file

@ -1,56 +0,0 @@
<template>
<FullCalendar :options="calendarOptions">
<template v-slot:eventContent="arg">
<span class="text-violet-3 dark:text-white font-bold">
{{ arg.event.title }}
</span>
</template>
</FullCalendar>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { locale } from "@/utils/i18n";
import { computed } from "vue";
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import iCalendarPlugin from "@fullcalendar/icalendar";
import interactionPlugin from "@fullcalendar/interaction";
const props = defineProps<{
icsFeedUrl: string;
}>();
const { t } = useI18n({ useScope: "global" });
const calendarOptions = computed((): object => {
return {
plugins: [dayGridPlugin, iCalendarPlugin, interactionPlugin],
initialView: "dayGridMonth",
events: {
url: props.icsFeedUrl,
format: "ics",
},
nextDayThreshold: "09:00:00",
moreLinkClassNames: "bg-mbz-yellow dark:bg-mbz-purple dark:text-white p-2",
moreLinkContent: (arg: { num: number; text: string }) => {
return "+" + arg.num.toString();
},
eventClassNames: "line-clamp-3 bg-mbz-yellow dark:bg-mbz-purple",
headerToolbar: {
left: "prev,next,today",
center: "title",
right: "dayGridWeek,dayGridMonth", // user can switch between the two
},
locale: locale,
firstDay: 1,
buttonText: {
today: t("Today"),
month: t("Month"),
week: t("Week"),
day: t("Day"),
list: t("List"),
},
};
});
</script>

View file

@ -201,6 +201,56 @@ export const SEARCH_EVENTS = gql`
${ACTOR_FRAGMENT}
`;
export const SEARCH_CALENDAR_EVENTS = gql`
query SearchEvents(
$beginsOn: DateTime
$endsOn: DateTime
$eventPage: Int
$limit: Int
) {
searchEvents(
beginsOn: $beginsOn
endsOn: $endsOn
page: $eventPage
limit: $limit
) {
total
elements {
id
title
uuid
beginsOn
endsOn
picture {
id
url
}
status
tags {
...TagFragment
}
physicalAddress {
...AdressFragment
}
organizerActor {
...ActorFragment
}
attributedTo {
...ActorFragment
}
options {
...EventOptions
}
__typename
}
}
}
${EVENT_OPTIONS_FRAGMENT}
${TAG_FRAGMENT}
${ADDRESS_FRAGMENT}
${ACTOR_FRAGMENT}
`;
export const SEARCH_GROUPS = gql`
query SearchGroups(
$location: String

View file

@ -5,21 +5,15 @@
</h1>
<div class="p-2">
<ICSCalendar
v-if="!isMobile"
ics-feed-url="https://rotes.potsda.mn/feed/instance/ics"
/>
<ICSAgenda
v-else
ics-feed-url="https://rotes.potsda.mn/feed/instance/ics"
/>
<EventsCalendar v-if="!isMobile" />
<EventsAgenda v-else />
</div>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import ICSCalendar from "@/components/FullCalendar/ICSCalendar.vue";
import ICSAgenda from "@/components/FullCalendar/ICSAgenda.vue";
import EventsAgenda from "@/components/FullCalendar/EventsAgenda.vue";
import EventsCalendar from "@/components/FullCalendar/EventsCalendar.vue";
const { t } = useI18n({ useScope: "global" });