feat(front): #1334 add a calendar view

This commit is contained in:
setop 2024-03-06 12:01:27 +01:00
commit e168fb307b
14 changed files with 515 additions and 8 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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

52
package-lock.json generated
View File

@ -12,6 +12,10 @@
"@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/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",
@ -57,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",
@ -2667,6 +2672,39 @@
"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/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",
@ -8215,6 +8253,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",
@ -10734,6 +10777,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",

View File

@ -36,6 +36,10 @@
"@framasoft/socket-apollo-link": "^1.0.0", "@framasoft/socket-apollo-link": "^1.0.0",
"@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",
"@fullcalendar/core": "^6.1.10",
"@fullcalendar/daygrid": "^6.1.10",
"@fullcalendar/interaction": "^6.1.10",
"@fullcalendar/vue3": "^6.1.10",
"@sentry/tracing": "^7.1", "@sentry/tracing": "^7.1",
"@sentry/vue": "^7.1", "@sentry/vue": "^7.1",
"@tiptap/core": "^2.0.0-beta.41", "@tiptap/core": "^2.0.0-beta.41",
@ -79,6 +83,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",

View File

@ -0,0 +1,228 @@
<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 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, 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>

View File

@ -0,0 +1,104 @@
<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 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, 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

@ -181,7 +181,21 @@
<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"
> >
<li v-if="currentActor?.id"> <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">
<router-link <router-link
:to="{ name: RouteName.MY_EVENTS }" :to="{ name: RouteName.MY_EVENTS }"
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" 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"
@ -209,6 +223,13 @@
>{{ t("Register") }}</router-link >{{ t("Register") }}</router-link
> >
</li> </li>
<search-fields
v-if="!showMobileMenu"
class="m-auto w-auto"
v-model:search="search"
v-model:location="location"
/>
</ul> </ul>
</div> </div>
</div> </div>
@ -219,19 +240,18 @@
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";

View File

@ -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,
};

View File

@ -206,6 +206,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

@ -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",

View File

@ -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",
@ -1648,4 +1649,4 @@
"You need to enter a text": "You need to enter a text", "You need to enter a text": "You need to enter a text",
"Error while adding tag: {error}": "Error while adding tag: {error}", "Error while adding tag: {error}": "Error while adding tag: {error}",
"From this instance only": "From this instance only" "From this instance only": "From this instance only"
} }

View File

@ -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,

View 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>