Merge remote-tracking branch 'potsdamn/feature/calendar'
This commit is contained in:
commit
4cdbf78037
|
@ -18,6 +18,8 @@ defmodule Mobilizon.Web.PageController do
|
||||||
defdelegate my_events(conn, params), to: PageController, as: :index
|
defdelegate my_events(conn, params), to: PageController, as: :index
|
||||||
@spec create_event(Plug.Conn.t(), any) :: Plug.Conn.t()
|
@spec create_event(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||||
defdelegate create_event(conn, params), to: PageController, as: :index
|
defdelegate create_event(conn, params), to: PageController, as: :index
|
||||||
|
@spec calendar(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||||
|
defdelegate calendar(conn, params), to: PageController, as: :index
|
||||||
@spec list_events(Plug.Conn.t(), any) :: Plug.Conn.t()
|
@spec list_events(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||||
defdelegate list_events(conn, params), to: PageController, as: :index
|
defdelegate list_events(conn, params), to: PageController, as: :index
|
||||||
@spec edit_event(Plug.Conn.t(), any) :: Plug.Conn.t()
|
@spec edit_event(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||||
|
|
|
@ -77,7 +77,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
|
||||||
# unsafe-eval is because of JS issues with regenerator-runtime
|
# unsafe-eval is because of JS issues with regenerator-runtime
|
||||||
@script_src "script-src 'self' 'unsafe-eval' "
|
@script_src "script-src 'self' 'unsafe-eval' "
|
||||||
@style_src "style-src 'self' "
|
@style_src "style-src 'self' "
|
||||||
@font_src "font-src 'self' "
|
@font_src "font-src 'self' data: "
|
||||||
|
|
||||||
@spec csp_string(Keyword.t()) :: String.t()
|
@spec csp_string(Keyword.t()) :: String.t()
|
||||||
defp csp_string(options) do
|
defp csp_string(options) do
|
||||||
|
@ -117,6 +117,8 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
|
||||||
|
|
||||||
style_src = [style_src] ++ [get_csp_config(:style_src, options)]
|
style_src = [style_src] ++ [get_csp_config(:style_src, options)]
|
||||||
|
|
||||||
|
style_src = [style_src] ++ ["'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"]
|
||||||
|
|
||||||
font_src = [@font_src] ++ [get_csp_config(:font_src, options)]
|
font_src = [@font_src] ++ [get_csp_config(:font_src, options)]
|
||||||
|
|
||||||
frame_src = build_csp_field(:frame_src, options)
|
frame_src = build_csp_field(:frame_src, options)
|
||||||
|
|
|
@ -120,6 +120,7 @@ defmodule Mobilizon.Web.Router do
|
||||||
get("/@:name", PageController, :actor)
|
get("/@:name", PageController, :actor)
|
||||||
get("/events/me", PageController, :my_events)
|
get("/events/me", PageController, :my_events)
|
||||||
get("/events/create", PageController, :create_event)
|
get("/events/create", PageController, :create_event)
|
||||||
|
get("/events/calendar", PageController, :calendar)
|
||||||
get("/events/:uuid", PageController, :event)
|
get("/events/:uuid", PageController, :event)
|
||||||
get("/comments/:uuid", PageController, :comment)
|
get("/comments/:uuid", PageController, :comment)
|
||||||
get("/resource/:uuid", PageController, :resource)
|
get("/resource/:uuid", PageController, :resource)
|
||||||
|
@ -188,6 +189,7 @@ defmodule Mobilizon.Web.Router do
|
||||||
get("/events/create", PageController, :create_event)
|
get("/events/create", PageController, :create_event)
|
||||||
get("/events/list", PageController, :list_events)
|
get("/events/list", PageController, :list_events)
|
||||||
get("/events/me", PageController, :my_events)
|
get("/events/me", PageController, :my_events)
|
||||||
|
get("/events/calendar", PageController, :calendar)
|
||||||
get("/events/:uuid/edit", PageController, :edit_event)
|
get("/events/:uuid/edit", PageController, :edit_event)
|
||||||
|
|
||||||
# This is a hack to ease link generation into emails
|
# This is a hack to ease link generation into emails
|
||||||
|
|
62
package-lock.json
generated
62
package-lock.json
generated
|
@ -11,6 +11,11 @@
|
||||||
"@apollo/client": "^3.3.16",
|
"@apollo/client": "^3.3.16",
|
||||||
"@framasoft/socket": "^1.0.0",
|
"@framasoft/socket": "^1.0.0",
|
||||||
"@framasoft/socket-apollo-link": "^1.0.0",
|
"@framasoft/socket-apollo-link": "^1.0.0",
|
||||||
|
"@fullcalendar/core": "^6.1.10",
|
||||||
|
"@fullcalendar/daygrid": "^6.1.10",
|
||||||
|
"@fullcalendar/icalendar": "^6.1.10",
|
||||||
|
"@fullcalendar/interaction": "^6.1.10",
|
||||||
|
"@fullcalendar/vue3": "^6.1.10",
|
||||||
"@oruga-ui/oruga-next": "^0.8.2",
|
"@oruga-ui/oruga-next": "^0.8.2",
|
||||||
"@oruga-ui/theme-oruga": "^0.2.0",
|
"@oruga-ui/theme-oruga": "^0.2.0",
|
||||||
"@sentry/tracing": "^7.1",
|
"@sentry/tracing": "^7.1",
|
||||||
|
@ -56,6 +61,7 @@
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"graphql-tag": "^2.10.3",
|
"graphql-tag": "^2.10.3",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
"ical.js": "^1.5.0",
|
||||||
"intersection-observer": "^0.12.0",
|
"intersection-observer": "^0.12.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"leaflet": "^1.4.0",
|
"leaflet": "^1.4.0",
|
||||||
|
@ -2649,6 +2655,48 @@
|
||||||
"zen-observable": "^0.10.0"
|
"zen-observable": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fullcalendar/core": {
|
||||||
|
"version": "6.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.10.tgz",
|
||||||
|
"integrity": "sha512-oTXGJSAGpCf1oY+CKp5qYjMHkJCPBkJ3SHitl63n8Q6xKeiwQ4EF6Au451euUovREwJpLmD1AyZrCnWmtB9AVg==",
|
||||||
|
"dependencies": {
|
||||||
|
"preact": "~10.12.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/daygrid": {
|
||||||
|
"version": "6.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.10.tgz",
|
||||||
|
"integrity": "sha512-Z4GRm1IyHKgxXFTWGcEI0nTsvYOIkpE0aMt3/o3ER2SZkF+hfwcDFhtj0c9+WhMjXFIWYeoTnA9rUOY7Zl/nxA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/icalendar": {
|
||||||
|
"version": "6.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/icalendar/-/icalendar-6.1.10.tgz",
|
||||||
|
"integrity": "sha512-TXjtZhjYIQZjeqULRjwDd2VWlymdhJmltaN26YS0dcGuCrQhJJ3x/sODVbVaW1mvbMjnjXYUE8AhdpxvhYGIJg==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.10",
|
||||||
|
"ical.js": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/interaction": {
|
||||||
|
"version": "6.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.10.tgz",
|
||||||
|
"integrity": "sha512-aZRlwCpmDasq2RNeWV0ub20Uevare9Cb6iMlxCacx0fhOC14H28G9d1FsduJIecInL84SPGwt5ItqAYMsWv7zw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/vue3": {
|
||||||
|
"version": "6.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/vue3/-/vue3-6.1.10.tgz",
|
||||||
|
"integrity": "sha512-YMYBQx0TlWNuN4G6ra2dkf5cCF5aVi/2zDLGLvLqe2Nk2o7uNbTkrCSG40061OepWQlJv+hYqm1JukLRmyqi4Q==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.10",
|
||||||
|
"vue": "^3.0.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@graphql-typed-document-node/core": {
|
"node_modules/@graphql-typed-document-node/core": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
|
||||||
|
@ -7962,6 +8010,11 @@
|
||||||
"url": "https://github.com/sponsors/typicode"
|
"url": "https://github.com/sponsors/typicode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ical.js": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ical.js/-/ical.js-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-7ZxMkogUkkaCx810yp0ZGKvq1ZpRgJeornPttpoxe6nYZ3NLesZe1wWMXDdwTkj/b5NtXT+Y16Aakph/ao98ZQ=="
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
@ -10310,6 +10363,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
|
||||||
|
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|
|
@ -31,6 +31,11 @@
|
||||||
"@apollo/client": "^3.3.16",
|
"@apollo/client": "^3.3.16",
|
||||||
"@framasoft/socket": "^1.0.0",
|
"@framasoft/socket": "^1.0.0",
|
||||||
"@framasoft/socket-apollo-link": "^1.0.0",
|
"@framasoft/socket-apollo-link": "^1.0.0",
|
||||||
|
"@fullcalendar/core": "^6.1.10",
|
||||||
|
"@fullcalendar/daygrid": "^6.1.10",
|
||||||
|
"@fullcalendar/icalendar": "^6.1.10",
|
||||||
|
"@fullcalendar/interaction": "^6.1.10",
|
||||||
|
"@fullcalendar/vue3": "^6.1.10",
|
||||||
"@oruga-ui/oruga-next": "^0.8.2",
|
"@oruga-ui/oruga-next": "^0.8.2",
|
||||||
"@oruga-ui/theme-oruga": "^0.2.0",
|
"@oruga-ui/theme-oruga": "^0.2.0",
|
||||||
"@sentry/tracing": "^7.1",
|
"@sentry/tracing": "^7.1",
|
||||||
|
@ -76,6 +81,7 @@
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"graphql-tag": "^2.10.3",
|
"graphql-tag": "^2.10.3",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
"ical.js": "^1.5.0",
|
||||||
"intersection-observer": "^0.12.0",
|
"intersection-observer": "^0.12.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"leaflet": "^1.4.0",
|
"leaflet": "^1.4.0",
|
||||||
|
|
229
src/components/FullCalendar/EventsAgenda.vue
Normal file
229
src/components/FullCalendar/EventsAgenda.vue
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
<template>
|
||||||
|
<FullCalendar
|
||||||
|
ref="calendarRef"
|
||||||
|
:options="calendarOptions"
|
||||||
|
class="agenda-view"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="listOfEventsByDate.date" class="my-4">
|
||||||
|
<b v-text="formatDateString(listOfEventsByDate.date)" />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EmptyContent v-else icon="calendar" :inline="true">
|
||||||
|
<span>
|
||||||
|
{{ t("No events found") }}
|
||||||
|
</span>
|
||||||
|
</EmptyContent>
|
||||||
|
</div>
|
||||||
|
</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 { EventSegment } from "@fullcalendar/core";
|
||||||
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
|
import iCalendarPlugin from "@fullcalendar/icalendar";
|
||||||
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
|
import {
|
||||||
|
formatDateISOStringWithoutTime,
|
||||||
|
formatDateString,
|
||||||
|
} from "@/filters/datetime";
|
||||||
|
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>(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: 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,
|
||||||
|
moreLinkClassNames: "bg-mbz-yellow dark:bg-mbz-purple dark:text-white",
|
||||||
|
moreLinkContent: (arg: { num: number; text: string }) => {
|
||||||
|
return "+" + arg.num.toString();
|
||||||
|
},
|
||||||
|
contentHeight: "auto",
|
||||||
|
eventClassNames: "line-clamp-3 bg-mbz-yellow dark:bg-mbz-purple",
|
||||||
|
headerToolbar: {
|
||||||
|
left: "prev,next,customTodayButton",
|
||||||
|
center: "",
|
||||||
|
right: "title",
|
||||||
|
},
|
||||||
|
locale: locale,
|
||||||
|
firstDay: 1,
|
||||||
|
buttonText: {
|
||||||
|
today: t("Today"),
|
||||||
|
month: t("Month"),
|
||||||
|
week: t("Week"),
|
||||||
|
day: t("Day"),
|
||||||
|
list: t("List"),
|
||||||
|
},
|
||||||
|
customButtons: {
|
||||||
|
customTodayButton: {
|
||||||
|
text: t("Today"),
|
||||||
|
click: () => {
|
||||||
|
calendarRef.value.getApi().today();
|
||||||
|
lastSelectedDate.value = formatDateISOStringWithoutTime(
|
||||||
|
new Date().toISOString()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dateClick: (info: { dateStr: string }) => {
|
||||||
|
showEventsByDate(info.dateStr);
|
||||||
|
},
|
||||||
|
moreLinkClick: (info: {
|
||||||
|
date: Date;
|
||||||
|
allSegs: EventSegment[];
|
||||||
|
hiddenSegs: EventSegment[];
|
||||||
|
jsEvent: object;
|
||||||
|
}) => {
|
||||||
|
listOfEventsByDate.value = {
|
||||||
|
events: info.allSegs,
|
||||||
|
date: info.date.toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (info.allSegs.length) {
|
||||||
|
window.location.hash =
|
||||||
|
"_" + formatDateISOStringWithoutTime(info.date.toISOString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return "none";
|
||||||
|
},
|
||||||
|
moreLinkDidMount: (arg: { el: Element }) => {
|
||||||
|
if (
|
||||||
|
lastSelectedDate.value &&
|
||||||
|
arg.el.closest(`td[data-date='${lastSelectedDate.value}']`)
|
||||||
|
) {
|
||||||
|
showEventsByDate(lastSelectedDate.value);
|
||||||
|
lastSelectedDate.value = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.agenda-view .fc-button {
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-view .fc-toolbar-title {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-view .fc-daygrid-day-events {
|
||||||
|
min-height: 1.1rem !important;
|
||||||
|
margin-bottom: 0.2rem !important;
|
||||||
|
margin-left: 0.1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agenda-view .fc-more-link {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-icon {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 0.95rem !important;
|
||||||
|
}
|
||||||
|
</style>
|
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>
|
|
@ -19,20 +19,18 @@
|
||||||
maxlength="1024"
|
maxlength="1024"
|
||||||
expanded
|
expanded
|
||||||
/>
|
/>
|
||||||
<o-button native-type="submit" icon-left="magnify">
|
<o-button native-type="submit" icon-left="magnify"> </o-button>
|
||||||
</o-button>
|
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { IAddress } from "@/types/address.model";
|
import { IAddress } from "@/types/address.model";
|
||||||
import { AddressSearchType } from "@/types/enums";
|
|
||||||
import { computed, defineAsyncComponent } from "vue";
|
import { computed, defineAsyncComponent } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import RouteName from "@/router/name";
|
import RouteName from "@/router/name";
|
||||||
|
|
||||||
const FullAddressAutoComplete = defineAsyncComponent(
|
defineAsyncComponent(
|
||||||
() => import("@/components/Event/FullAddressAutoComplete.vue")
|
() => import("@/components/Event/FullAddressAutoComplete.vue")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -165,9 +165,20 @@
|
||||||
<ul
|
<ul
|
||||||
class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold"
|
class="flex flex-col md:flex-row md:space-x-8 mt-2 md:mt-0 md:font-lightbold"
|
||||||
>
|
>
|
||||||
|
<search-fields
|
||||||
|
v-if="showMobileMenu"
|
||||||
|
class="m-auto w-auto"
|
||||||
|
v-model:search="search"
|
||||||
|
v-model:location="location"
|
||||||
|
/>
|
||||||
|
|
||||||
<search-fields v-if="showMobileMenu" class="m-auto w-auto" v-model:search="search" v-model:location="location"/>
|
<li class="m-auto">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: RouteName.EVENT_CALENDAR }"
|
||||||
|
class="block py-2 pr-4 pl-3 text-zinc-700 border-b border-gray-100 hover:bg-zinc-50 md:hover:bg-transparent md:border-0 md:hover:text-mbz-purple-700 md:p-0 dark:text-zinc-400 md:dark:hover:text-white dark:hover:bg-zinc-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
>{{ t("Calendar") }}</router-link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
<li class="m-auto" v-if="currentActor?.id">
|
<li class="m-auto" v-if="currentActor?.id">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: RouteName.MY_EVENTS }"
|
:to="{ name: RouteName.MY_EVENTS }"
|
||||||
|
@ -197,8 +208,12 @@
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<search-fields v-if="!showMobileMenu" class="m-auto w-auto" v-model:search="search" v-model:location="location"/>
|
<search-fields
|
||||||
|
v-if="!showMobileMenu"
|
||||||
|
class="m-auto w-auto"
|
||||||
|
v-model:search="search"
|
||||||
|
v-model:location="location"
|
||||||
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -209,29 +224,23 @@
|
||||||
import MobilizonLogo from "@/components/MobilizonLogo.vue";
|
import MobilizonLogo from "@/components/MobilizonLogo.vue";
|
||||||
import { ICurrentUserRole } from "@/types/enums";
|
import { ICurrentUserRole } from "@/types/enums";
|
||||||
import { logout } from "../utils/auth";
|
import { logout } from "../utils/auth";
|
||||||
import { IPerson, displayName } from "../types/actor";
|
import { displayName } from "../types/actor";
|
||||||
import RouteName from "../router/name";
|
import RouteName from "../router/name";
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||||
import Inbox from "vue-material-design-icons/Inbox.vue";
|
|
||||||
import { useCurrentUserClient } from "@/composition/apollo/user";
|
import { useCurrentUserClient } from "@/composition/apollo/user";
|
||||||
import {
|
import {
|
||||||
useCurrentActorClient,
|
useCurrentActorClient,
|
||||||
useCurrentUserIdentities,
|
useCurrentUserIdentities,
|
||||||
} from "@/composition/apollo/actor";
|
} from "@/composition/apollo/actor";
|
||||||
import { useLazyQuery, useMutation } from "@vue/apollo-composable";
|
import { useMutation } from "@vue/apollo-composable";
|
||||||
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
|
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
|
||||||
import { changeIdentity } from "@/utils/identity";
|
import { changeIdentity } from "@/utils/identity";
|
||||||
import { useRegistrationConfig } from "@/composition/apollo/config";
|
import { useRegistrationConfig } from "@/composition/apollo/config";
|
||||||
import { useOruga } from "@oruga-ui/oruga-next";
|
import { useOruga } from "@oruga-ui/oruga-next";
|
||||||
import SearchFields from "@/components/Home/SearchFields.vue";
|
import SearchFields from "@/components/Home/SearchFields.vue";
|
||||||
import {
|
|
||||||
UNREAD_ACTOR_CONVERSATIONS,
|
|
||||||
UNREAD_ACTOR_CONVERSATIONS_SUBSCRIPTION,
|
|
||||||
} from "@/graphql/user";
|
|
||||||
import { ICurrentUser } from "@/types/current-user.model";
|
|
||||||
|
|
||||||
const { currentUser } = useCurrentUserClient();
|
const { currentUser } = useCurrentUserClient();
|
||||||
const { currentActor } = useCurrentActorClient();
|
const { currentActor } = useCurrentActorClient();
|
||||||
|
|
|
@ -4,6 +4,10 @@ function parseDateTime(value: string): Date {
|
||||||
return new Date(value);
|
return new Date(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateISOStringWithoutTime(value: string): string {
|
||||||
|
return parseDateTime(value).toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
function formatDateString(value: string): string {
|
function formatDateString(value: string): string {
|
||||||
return parseDateTime(value).toLocaleString(locale(), {
|
return parseDateTime(value).toLocaleString(locale(), {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
|
@ -76,4 +80,9 @@ function formatDateTimeString(
|
||||||
|
|
||||||
const locale = () => i18n.global.locale.replace("_", "-");
|
const locale = () => i18n.global.locale.replace("_", "-");
|
||||||
|
|
||||||
export { formatDateString, formatTimeString, formatDateTimeString };
|
export {
|
||||||
|
formatDateISOStringWithoutTime,
|
||||||
|
formatDateString,
|
||||||
|
formatTimeString,
|
||||||
|
formatDateTimeString,
|
||||||
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -168,6 +168,7 @@
|
||||||
"By transit": "Mit öffentlichen Verkehrsmitteln",
|
"By transit": "Mit öffentlichen Verkehrsmitteln",
|
||||||
"By {group}": "Von {group}",
|
"By {group}": "Von {group}",
|
||||||
"By {username}": "Von {username}",
|
"By {username}": "Von {username}",
|
||||||
|
"Calendar": "Kalender",
|
||||||
"Can be an email or a link, or just plain text.": "Dies kann eine E-Mail-Adresse oder ein Link sein. Oder einfach ein Freitext.",
|
"Can be an email or a link, or just plain text.": "Dies kann eine E-Mail-Adresse oder ein Link sein. Oder einfach ein Freitext.",
|
||||||
"Cancel": "Abbrechen",
|
"Cancel": "Abbrechen",
|
||||||
"Cancel anonymous participation": "Anonyme Teilnahme stornieren",
|
"Cancel anonymous participation": "Anonyme Teilnahme stornieren",
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"Back to previous page": "Back to previous page",
|
"Back to previous page": "Back to previous page",
|
||||||
"Before you can login, you need to click on the link inside it to validate your account.": "Before you can login, you need to click on the link inside it to validate your account.",
|
"Before you can login, you need to click on the link inside it to validate your account.": "Before you can login, you need to click on the link inside it to validate your account.",
|
||||||
"By {username}": "By {username}",
|
"By {username}": "By {username}",
|
||||||
|
"Calendar": "Calendar",
|
||||||
"Cancel anonymous participation": "Cancel anonymous participation",
|
"Cancel anonymous participation": "Cancel anonymous participation",
|
||||||
"Cancel creation": "Cancel creation",
|
"Cancel creation": "Cancel creation",
|
||||||
"Cancel edition": "Cancel edition",
|
"Cancel edition": "Cancel edition",
|
||||||
|
|
|
@ -7,9 +7,11 @@ const participations = () => import("@/views/Event/ParticipantsView.vue");
|
||||||
const editEvent = () => import("@/views/Event/EditView.vue");
|
const editEvent = () => import("@/views/Event/EditView.vue");
|
||||||
const event = () => import("@/views/Event/EventView.vue");
|
const event = () => import("@/views/Event/EventView.vue");
|
||||||
const myEvents = () => import("@/views/Event/MyEventsView.vue");
|
const myEvents = () => import("@/views/Event/MyEventsView.vue");
|
||||||
|
const eventCalendar = () => import("@/views/Event/CalendarView.vue");
|
||||||
|
|
||||||
export enum EventRouteName {
|
export enum EventRouteName {
|
||||||
EVENT_LIST = "EventList",
|
EVENT_LIST = "EventList",
|
||||||
|
EVENT_CALENDAR = "EventCalendar",
|
||||||
CREATE_EVENT = "CreateEvent",
|
CREATE_EVENT = "CreateEvent",
|
||||||
MY_EVENTS = "MyEvents",
|
MY_EVENTS = "MyEvents",
|
||||||
EDIT_EVENT = "EditEvent",
|
EDIT_EVENT = "EditEvent",
|
||||||
|
@ -26,6 +28,14 @@ export enum EventRouteName {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventRoutes: RouteRecordRaw[] = [
|
export const eventRoutes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: "/events/calendar",
|
||||||
|
name: EventRouteName.EVENT_CALENDAR,
|
||||||
|
component: eventCalendar,
|
||||||
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/events/create",
|
path: "/events/create",
|
||||||
name: EventRouteName.CREATE_EVENT,
|
name: EventRouteName.CREATE_EVENT,
|
||||||
|
|
21
src/views/Event/CalendarView.vue
Normal file
21
src/views/Event/CalendarView.vue
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div class="container mx-auto px-1 mb-6">
|
||||||
|
<h1 v-if="!isMobile">
|
||||||
|
{{ t("Calendar") }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="p-2">
|
||||||
|
<EventsCalendar v-if="!isMobile" />
|
||||||
|
<EventsAgenda v-else />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import EventsAgenda from "@/components/FullCalendar/EventsAgenda.vue";
|
||||||
|
import EventsCalendar from "@/components/FullCalendar/EventsCalendar.vue";
|
||||||
|
|
||||||
|
const { t } = useI18n({ useScope: "global" });
|
||||||
|
|
||||||
|
const isMobile = window.innerWidth < 760;
|
||||||
|
</script>
|
Loading…
Reference in a new issue