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

View file

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