Merge remote-tracking branch 'potsdamn/feature/calendar'

This commit is contained in:
778a69cd 2024-02-01 00:40:38 +01:00
commit 4cdbf78037
15 changed files with 527 additions and 20 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

62
package-lock.json generated
View file

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

View file

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

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

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

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

View file

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

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

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

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

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>