forked from potsda.mn/mobilizon
Implement graphql query for events calendar and agenda components
- remove ICSCalendar and ICSAgenda components - fix highlight selected date potsda.mn/mobilizon#40 potsda.mn/mobilizon#41
This commit is contained in:
parent
dec26525c0
commit
a48b315f16
|
@ -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;
|
105
src/components/FullCalendar/EventsCalendar.vue
Normal file
105
src/components/FullCalendar/EventsCalendar.vue
Normal 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>
|
|
@ -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>
|
|
|
@ -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
|
||||||
|
|
|
@ -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" });
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue