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
|
||||
@spec create_event(Plug.Conn.t(), any) :: Plug.Conn.t()
|
||||
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()
|
||||
defdelegate list_events(conn, params), to: PageController, as: :index
|
||||
@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
|
||||
@script_src "script-src 'self' 'unsafe-eval' "
|
||||
@style_src "style-src 'self' "
|
||||
@font_src "font-src 'self' "
|
||||
@font_src "font-src 'self' data: "
|
||||
|
||||
@spec csp_string(Keyword.t()) :: String.t()
|
||||
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] ++ ["'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='"]
|
||||
|
||||
font_src = [@font_src] ++ [get_csp_config(:font_src, options)]
|
||||
|
||||
frame_src = build_csp_field(:frame_src, options)
|
||||
|
|
|
@ -120,6 +120,7 @@ defmodule Mobilizon.Web.Router do
|
|||
get("/@:name", PageController, :actor)
|
||||
get("/events/me", PageController, :my_events)
|
||||
get("/events/create", PageController, :create_event)
|
||||
get("/events/calendar", PageController, :calendar)
|
||||
get("/events/:uuid", PageController, :event)
|
||||
get("/comments/:uuid", PageController, :comment)
|
||||
get("/resource/:uuid", PageController, :resource)
|
||||
|
@ -188,6 +189,7 @@ defmodule Mobilizon.Web.Router do
|
|||
get("/events/create", PageController, :create_event)
|
||||
get("/events/list", PageController, :list_events)
|
||||
get("/events/me", PageController, :my_events)
|
||||
get("/events/calendar", PageController, :calendar)
|
||||
get("/events/:uuid/edit", PageController, :edit_event)
|
||||
|
||||
# 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",
|
||||
"@framasoft/socket": "^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/theme-oruga": "^0.2.0",
|
||||
"@sentry/tracing": "^7.1",
|
||||
|
@ -56,6 +61,7 @@
|
|||
"graphql": "^16.8.1",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"ical.js": "^1.5.0",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"leaflet": "^1.4.0",
|
||||
|
@ -2649,6 +2655,48 @@
|
|||
"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": {
|
||||
"version": "3.2.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "0.6.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
|
@ -31,6 +31,11 @@
|
|||
"@apollo/client": "^3.3.16",
|
||||
"@framasoft/socket": "^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/theme-oruga": "^0.2.0",
|
||||
"@sentry/tracing": "^7.1",
|
||||
|
@ -76,6 +81,7 @@
|
|||
"graphql": "^16.8.1",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"ical.js": "^1.5.0",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"jwt-decode": "^4.0.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"
|
||||
expanded
|
||||
/>
|
||||
<o-button native-type="submit" icon-left="magnify">
|
||||
</o-button>
|
||||
<o-button native-type="submit" icon-left="magnify"> </o-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { IAddress } from "@/types/address.model";
|
||||
import { AddressSearchType } from "@/types/enums";
|
||||
import { computed, defineAsyncComponent } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import RouteName from "@/router/name";
|
||||
|
||||
const FullAddressAutoComplete = defineAsyncComponent(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/Event/FullAddressAutoComplete.vue")
|
||||
);
|
||||
|
||||
|
|
|
@ -165,9 +165,20 @@
|
|||
<ul
|
||||
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">
|
||||
<router-link
|
||||
:to="{ name: RouteName.MY_EVENTS }"
|
||||
|
@ -197,8 +208,12 @@
|
|||
>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -209,29 +224,23 @@
|
|||
import MobilizonLogo from "@/components/MobilizonLogo.vue";
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { logout } from "../utils/auth";
|
||||
import { IPerson, displayName } from "../types/actor";
|
||||
import { displayName } from "../types/actor";
|
||||
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 { useI18n } from "vue-i18n";
|
||||
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 {
|
||||
useCurrentActorClient,
|
||||
useCurrentUserIdentities,
|
||||
} 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 { changeIdentity } from "@/utils/identity";
|
||||
import { useRegistrationConfig } from "@/composition/apollo/config";
|
||||
import { useOruga } from "@oruga-ui/oruga-next";
|
||||
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 { currentActor } = useCurrentActorClient();
|
||||
|
|
|
@ -4,6 +4,10 @@ function parseDateTime(value: string): Date {
|
|||
return new Date(value);
|
||||
}
|
||||
|
||||
function formatDateISOStringWithoutTime(value: string): string {
|
||||
return parseDateTime(value).toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function formatDateString(value: string): string {
|
||||
return parseDateTime(value).toLocaleString(locale(), {
|
||||
weekday: "long",
|
||||
|
@ -76,4 +80,9 @@ function formatDateTimeString(
|
|||
|
||||
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}
|
||||
`;
|
||||
|
||||
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`
|
||||
query SearchGroups(
|
||||
$location: String
|
||||
|
|
|
@ -168,6 +168,7 @@
|
|||
"By transit": "Mit öffentlichen Verkehrsmitteln",
|
||||
"By {group}": "Von {group}",
|
||||
"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.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Cancel anonymous participation": "Anonyme Teilnahme stornieren",
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"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.",
|
||||
"By {username}": "By {username}",
|
||||
"Calendar": "Calendar",
|
||||
"Cancel anonymous participation": "Cancel anonymous participation",
|
||||
"Cancel creation": "Cancel creation",
|
||||
"Cancel edition": "Cancel edition",
|
||||
|
@ -1643,4 +1644,4 @@
|
|||
"Software details: {software_details}": "Software details: {software_details}",
|
||||
"Only instances with an application actor can be followed": "Only instances with an application actor can be followed",
|
||||
"Domain or instance name": "Domain or instance name"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,11 @@ const participations = () => import("@/views/Event/ParticipantsView.vue");
|
|||
const editEvent = () => import("@/views/Event/EditView.vue");
|
||||
const event = () => import("@/views/Event/EventView.vue");
|
||||
const myEvents = () => import("@/views/Event/MyEventsView.vue");
|
||||
const eventCalendar = () => import("@/views/Event/CalendarView.vue");
|
||||
|
||||
export enum EventRouteName {
|
||||
EVENT_LIST = "EventList",
|
||||
EVENT_CALENDAR = "EventCalendar",
|
||||
CREATE_EVENT = "CreateEvent",
|
||||
MY_EVENTS = "MyEvents",
|
||||
EDIT_EVENT = "EditEvent",
|
||||
|
@ -26,6 +28,14 @@ export enum EventRouteName {
|
|||
}
|
||||
|
||||
export const eventRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/events/calendar",
|
||||
name: EventRouteName.EVENT_CALENDAR,
|
||||
component: eventCalendar,
|
||||
meta: {
|
||||
requiredAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/events/create",
|
||||
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