From 12963317963f6aa00d9339ee5516e5f42c3f4c79 Mon Sep 17 00:00:00 2001 From: Julian Krauser <jkrauser209@gmail.com> Date: Mon, 14 Apr 2025 09:12:00 +0200 Subject: [PATCH 1/2] extend calendar by list --- package-lock.json | 10 ++ package.json | 1 + src/components/CustomCalendar.vue | 170 +++++++++++++++++++++ src/main.css | 28 +--- src/views/admin/club/calendar/Calendar.vue | 103 ++----------- src/views/public/calendar/Calendar.vue | 38 +---- 6 files changed, 198 insertions(+), 152 deletions(-) create mode 100644 src/components/CustomCalendar.vue diff --git a/package-lock.json b/package-lock.json index f1e26a7..a08af4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@fullcalendar/core": "^6.1.15", "@fullcalendar/daygrid": "^6.1.15", "@fullcalendar/interaction": "^6.1.15", + "@fullcalendar/list": "^6.1.17", "@fullcalendar/timegrid": "^6.1.15", "@fullcalendar/vue3": "^6.1.15", "@headlessui/vue": "^1.7.23", @@ -2379,6 +2380,15 @@ "@fullcalendar/core": "~6.1.17" } }, + "node_modules/@fullcalendar/list": { + "version": "6.1.17", + "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.17.tgz", + "integrity": "sha512-fkyK49F9IxwlGUBVhJGsFpd/LTi/vRVERLIAe1HmBaGkjwpxnynm8TMLb9mZip97wvDk3CmZWduMe6PxscAlow==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.17" + } + }, "node_modules/@fullcalendar/timegrid": { "version": "6.1.17", "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.17.tgz", diff --git a/package.json b/package.json index 4ffd7d2..aa87b2c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@fullcalendar/core": "^6.1.15", "@fullcalendar/daygrid": "^6.1.15", "@fullcalendar/interaction": "^6.1.15", + "@fullcalendar/list": "^6.1.17", "@fullcalendar/timegrid": "^6.1.15", "@fullcalendar/vue3": "^6.1.15", "@headlessui/vue": "^1.7.23", diff --git a/src/components/CustomCalendar.vue b/src/components/CustomCalendar.vue new file mode 100644 index 0000000..e31d2d6 --- /dev/null +++ b/src/components/CustomCalendar.vue @@ -0,0 +1,170 @@ +<template> + <div class="flex flex-col w-full h-full gap-2 justify-between overflow-hidden"> + <div class="flex flex-row gap-2 max-xl:flex-wrap justify-between max-sm:justify-center"> + <div class="flex flex-row max-xl:order-2"> + <button + :primary="view == 'dayGridMonth'" + :primary-outline="view != 'dayGridMonth'" + class="rounded-r-none!" + @click="setView('dayGridMonth')" + > + Monat + </button> + <button + :primary="view == 'timeGridWeek'" + :primary-outline="view != 'timeGridWeek'" + class="rounded-none! border-x-0!" + @click="setView('timeGridWeek')" + > + Woche + </button> + <button + :primary="view == 'listMonth'" + :primary-outline="view != 'listMonth'" + class="rounded-l-none!" + @click="setView('listMonth')" + > + Liste + </button> + </div> + <p class="text-3xl w-full max-xl:order-1 text-center">{{ currentTitle }}</p> + <div class="flex flex-row max-xl:order-3"> + <button primary-outline class="rounded-r-none!" @click="navigateView('prev')"> + <ChevronLeftIcon /> + </button> + <button + :primary="containsToday" + :primary-outline="!containsToday" + class="rounded-none! border-x-0!" + @click=" + calendarApi?.today(); + containsToday = true; + " + > + heute + </button> + <button primary-outline class="rounded-l-none!" @click="navigateView('next')"> + <ChevronRightIcon /> + </button> + </div> + </div> + <FullCalendar ref="fullCalendar" :options="calendarOptions" class="max-h-full h-full" /> + </div> +</template> + +<script setup lang="ts"> +import { defineComponent, type PropType } from "vue"; +import FullCalendar from "@fullcalendar/vue3"; +import deLocale from "@fullcalendar/core/locales/de"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import timeGridPlugin from "@fullcalendar/timegrid"; +import listPlugin from "@fullcalendar/list"; +import interactionPlugin from "@fullcalendar/interaction"; +import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/24/outline"; +import type { CalendarOptions } from "@fullcalendar/core/index.js"; +</script> + +<script lang="ts"> +export default defineComponent({ + props: { + items: { + type: Array as PropType< + { + id: string; + title: string; + start: string; + end: string; + backgroundColor: string; + }[] + >, + default: [], + }, + allowInteraction: { + type: Boolean, + default: true, + }, + }, + emits: { + dateSelect: ({ start, end, allDay }: { start: string; end: string; allDay: boolean }) => { + return typeof start == "string" && typeof end == "string" && typeof allDay === "boolean"; + }, + eventSelect: (id: string) => { + return typeof id == "string"; + }, + }, + data() { + return { + view: "dayGridMonth" as "dayGridMonth" | "timeGridWeek" | "listMonth", + calendarApi: null as null | typeof FullCalendar, + currentTitle: "" as string, + containsToday: false as boolean, + }; + }, + computed: { + calendarOptions() { + return { + timeZone: "local", + locale: deLocale, + plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin], + initialView: "dayGridMonth", + eventDisplay: "block", + headerToolbar: false, + weekends: true, + editable: this.allowInteraction, + selectable: this.allowInteraction, + selectMirror: false, + dayMaxEvents: true, + weekNumbers: true, + displayEventTime: true, + nowIndicator: true, + weekText: "KW", + allDaySlot: false, + events: this.items, + select: this.select, + eventClick: this.eventClick, + } as CalendarOptions; + }, + }, + mounted() { + this.calendarApi = (this.$refs.fullCalendar as typeof FullCalendar).getApi(); + this.setTitle(); + this.setContainsToday(); + }, + methods: { + setTitle() { + this.currentTitle = this.calendarApi?.view.title ?? ""; + }, + setView(view: "dayGridMonth" | "timeGridWeek" | "listMonth") { + this.calendarApi?.changeView(view); + this.view = view; + this.setTitle(); + this.setContainsToday(); + }, + navigateView(change: "prev" | "next") { + if (change == "prev") { + this.calendarApi?.prev(); + } else { + this.calendarApi?.next(); + } + this.setTitle(); + this.setContainsToday(); + }, + setContainsToday() { + const start = this.calendarApi?.view.currentStart; + const end = this.calendarApi?.view.currentEnd; + const today = new Date(); + this.containsToday = today >= start && today < end; + }, + select(e: any) { + this.$emit("dateSelect", { + start: e?.startStr ?? new Date().toISOString(), + end: e?.endStr ?? new Date().toISOString(), + allDay: e?.allDay ?? false, + }); + }, + eventClick(e: any) { + this.$emit("eventSelect", e.event.id); + }, + }, +}); +</script> diff --git a/src/main.css b/src/main.css index 9816a0e..e618f2f 100644 --- a/src/main.css +++ b/src/main.css @@ -72,7 +72,7 @@ a[button] { button[primary]:not([primary="false"]), a[button][primary]:not([primary="false"]) { - @apply border border-transparent text-white bg-primary hover:bg-primary; + @apply border-2 border-transparent text-white bg-primary hover:bg-primary; } button[primary-outline]:not([primary-outline="false"]), @@ -131,29 +131,3 @@ summary > svg { summary::-webkit-details-marker { display: none; } - -.fc-button-primary { - @apply bg-primary! border-primary! outline-hidden! ring-0! hover:bg-red-700! hover:border-red-700! h-10 text-center; -} -.fc-button-active { - @apply bg-red-500! border-red-500!; -} -.fc-toolbar { - @apply flex-wrap; -} - -/* For screens between 850px and 768px */ -@media (max-width: 850px) and (min-width: 768px) { - .fc-header-toolbar.fc-toolbar.fc-toolbar-ltr > .fc-toolbar-chunk:nth-child(2) { - @apply order-1!; - } - /* Your styles for this range */ -} - -/* For screens between 525px and 0px */ -@media (max-width: 525px) and (min-width: 0px) { - /* Your styles for this range */ - .fc-header-toolbar.fc-toolbar.fc-toolbar-ltr > .fc-toolbar-chunk:nth-child(2) { - @apply order-1!; - } -} diff --git a/src/views/admin/club/calendar/Calendar.vue b/src/views/admin/club/calendar/Calendar.vue index d45bd41..4535be8 100644 --- a/src/views/admin/club/calendar/Calendar.vue +++ b/src/views/admin/club/calendar/Calendar.vue @@ -4,14 +4,17 @@ <div class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <h1 class="font-bold text-xl h-8">Kalender</h1> <div class="flex flex-row gap-2"> - <PlusIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="select" /> + <PlusIcon + class="text-gray-500 h-5 w-5 cursor-pointer" + @click="select({ start: '', end: '', allDay: false })" + /> <LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" /> </div> </div> </template> <template #diffMain> <div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden"> - <FullCalendar :options="calendarOptions" class="max-h-full h-full" /> + <CustomCalendar :items="formattedItems" @date-select="select" @event-select="eventClick" /> </div> </template> </MainTemplate> @@ -22,14 +25,10 @@ import { defineComponent, markRaw, defineAsyncComponent } from "vue"; import { mapActions, mapState } from "pinia"; import { useModalStore } from "@/stores/modal"; import MainTemplate from "@/templates/Main.vue"; -import FullCalendar from "@fullcalendar/vue3"; -import deLocale from "@fullcalendar/core/locales/de"; -import dayGridPlugin from "@fullcalendar/daygrid"; -import timeGridPlugin from "@fullcalendar/timegrid"; -import interactionPlugin from "@fullcalendar/interaction"; import { useCalendarStore } from "@/stores/admin/club/calendar"; import { useAbilityStore } from "@/stores/ability"; import { LinkIcon, PlusIcon } from "@heroicons/vue/24/outline"; +import CustomCalendar from "@/components/CustomCalendar.vue"; </script> <script lang="ts"> @@ -40,33 +39,6 @@ export default defineComponent({ computed: { ...mapState(useCalendarStore, ["formattedItems"]), ...mapState(useAbilityStore, ["can"]), - calendarOptions() { - return { - timeZone: "local", - locale: deLocale, - plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin], - initialView: "dayGridMonth", - headerToolbar: { - left: "dayGridMonth,timeGridWeek", - center: "title", - right: "prev,today,next", - }, - eventDisplay: "block", - weekends: true, - editable: true, - selectable: true, - selectMirror: false, - dayMaxEvents: true, - weekNumbers: true, - displayEventTime: true, - nowIndicator: true, - weekText: "KW", - allDaySlot: false, - events: this.formattedItems, - select: this.select, - eventClick: this.eventClick, - }; - }, }, mounted() { this.fetchCalendars(); @@ -74,22 +46,22 @@ export default defineComponent({ methods: { ...mapActions(useModalStore, ["openModal"]), ...mapActions(useCalendarStore, ["fetchCalendars"]), - select(e: any) { + select({ start, end, allDay }: { start: string; end: string; allDay: boolean }) { if (!this.can("create", "club", "calendar")) return; this.openModal( markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/CreateCalendarModal.vue"))), { - start: e?.startStr ?? new Date().toISOString(), - end: e?.endStr ?? new Date().toISOString(), - allDay: e?.allDay ?? false, + start, + end, + allDay, } ); }, - eventClick(e: any) { + eventClick(id: string) { if (!this.can("update", "club", "calendar")) return; this.openModal( markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/UpdateCalendarModal.vue"))), - e.event.id + id ); }, openLinkModal(e: any) { @@ -99,55 +71,4 @@ export default defineComponent({ }, }, }); - -/** -locale: deLocale, - events: this.absencesList.map((x) => ({ - id: x.absenceId, - start: x.startDate, - end: x.endDate, - allday: true, - backgroundColor: this.getColorForAbsenceType(x.absenceType), - borderColor: '#ffffff', - title: this.getAbsenceType(x.absenceType) + ' ' + x.fullName, - })), - plugins: [ - interactionPlugin, - dayGridPlugin, - timeGridPlugin, - listPlugin, - multiMonthPlugin, - ], - initialView: 'dayGridMonth', - eventDisplay: 'block', - weekends: false, - editable: true, - selectable: true, - selectMirror: true, - dayMaxEvents: true, - weekNumbers: true, - displayEventTime: false, - weekText: 'KW', - validRange: { start: '2023-01-01', end: '' }, - headerToolbar: { - left: 'today prev,next', - center: 'title', - right: 'listMonth,dayGridMonth,multiMonthYear,customview', - }, - views: { - customview: { - type: 'multiMonth', - multiMonthMaxColumns: 1, - duration: { month: 12 }, - buttonText: 'grid', - }, - }, - dateClick: this.handleDateSelect.bind(this), - datesSet: this.handleMonthChange.bind(this), - select: this.handleDateSelect.bind(this), - eventClick: this.handleEventClick.bind(this), - eventsSet: this.handleEvents.bind(this), - }; - - */ </script> diff --git a/src/views/public/calendar/Calendar.vue b/src/views/public/calendar/Calendar.vue index 956a34d..f4de6da 100644 --- a/src/views/public/calendar/Calendar.vue +++ b/src/views/public/calendar/Calendar.vue @@ -12,7 +12,7 @@ </template> <template #diffMain> <div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden"> - <FullCalendar :options="calendarOptions" class="max-h-full h-full" /> + <CustomCalendar :items="formattedItems" :allow-interaction="false" @event-select="eventClick" /> </div> </template> </MainTemplate> @@ -23,14 +23,10 @@ import { defineComponent, markRaw, defineAsyncComponent } from "vue"; import { mapActions, mapState } from "pinia"; import { useModalStore } from "@/stores/modal"; import MainTemplate from "@/templates/Main.vue"; -import FullCalendar from "@fullcalendar/vue3"; -import deLocale from "@fullcalendar/core/locales/de"; -import dayGridPlugin from "@fullcalendar/daygrid"; -import timeGridPlugin from "@fullcalendar/timegrid"; -import interactionPlugin from "@fullcalendar/interaction"; import { InformationCircleIcon, LinkIcon } from "@heroicons/vue/24/outline"; import type { CalendarViewModel } from "@/viewmodels/admin/club/calendar.models"; import { RouterLink } from "vue-router"; +import CustomCalendar from "@/components/CustomCalendar.vue"; </script> <script lang="ts"> @@ -50,32 +46,6 @@ export default defineComponent({ backgroundColor: c.type.color, })); }, - calendarOptions() { - return { - timeZone: "local", - locale: deLocale, - plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin], - initialView: "dayGridMonth", - headerToolbar: { - left: "dayGridMonth,timeGridWeek", - center: "title", - right: "prev,today,next", - }, - eventDisplay: "block", - weekends: true, - editable: false, - selectable: false, - selectMirror: false, - dayMaxEvents: true, - weekNumbers: true, - displayEventTime: true, - nowIndicator: true, - weekText: "KW", - allDaySlot: false, - events: this.formattedItems, - eventClick: this.eventClick, - }; - }, }, mounted() { this.fetchCalendars(); @@ -93,10 +63,10 @@ export default defineComponent({ openLinkModal(e: any) { this.openModal(markRaw(defineAsyncComponent(() => import("@/components/public/calendar/CalendarLinkModal.vue")))); }, - eventClick(e: any) { + eventClick(id: string) { this.openModal( markRaw(defineAsyncComponent(() => import("@/components/public/calendar/ShowCalendarEntryModal.vue"))), - this.calendars.find((c) => c.id == e.event.id) + this.calendars.find((c) => c.id == id) ); }, }, From 6d453255433ba532868dbb2739fa24e34decd18b Mon Sep 17 00:00:00 2001 From: Julian Krauser <jkrauser209@gmail.com> Date: Mon, 14 Apr 2025 09:17:32 +0200 Subject: [PATCH 2/2] add styling option for wide calendar view --- src/components/CustomCalendar.vue | 17 +++++++++++++---- src/views/public/calendar/Calendar.vue | 7 ++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/CustomCalendar.vue b/src/components/CustomCalendar.vue index e31d2d6..edafd21 100644 --- a/src/components/CustomCalendar.vue +++ b/src/components/CustomCalendar.vue @@ -1,7 +1,10 @@ <template> <div class="flex flex-col w-full h-full gap-2 justify-between overflow-hidden"> - <div class="flex flex-row gap-2 max-xl:flex-wrap justify-between max-sm:justify-center"> - <div class="flex flex-row max-xl:order-2"> + <div + class="flex flex-row gap-2 justify-between max-sm:justify-center" + :class="smallStyling ? 'max-lg:flex-wrap' : 'max-xl:flex-wrap'" + > + <div class="flex flex-row" :class="smallStyling ? 'max-lg:order-2' : 'max-xl:order-2'"> <button :primary="view == 'dayGridMonth'" :primary-outline="view != 'dayGridMonth'" @@ -27,8 +30,10 @@ Liste </button> </div> - <p class="text-3xl w-full max-xl:order-1 text-center">{{ currentTitle }}</p> - <div class="flex flex-row max-xl:order-3"> + <p class="text-3xl w-full text-center" :class="smallStyling ? 'max-lg:order-1' : 'max-xl:order-1'"> + {{ currentTitle }} + </p> + <div class="flex flex-row" :class="smallStyling ? 'max-lg:order-3' : 'max-xl:order-3'"> <button primary-outline class="rounded-r-none!" @click="navigateView('prev')"> <ChevronLeftIcon /> </button> @@ -83,6 +88,10 @@ export default defineComponent({ type: Boolean, default: true, }, + smallStyling: { + type: Boolean, + default: false, + }, }, emits: { dateSelect: ({ start, end, allDay }: { start: string; end: string; allDay: boolean }) => { diff --git a/src/views/public/calendar/Calendar.vue b/src/views/public/calendar/Calendar.vue index f4de6da..8c68586 100644 --- a/src/views/public/calendar/Calendar.vue +++ b/src/views/public/calendar/Calendar.vue @@ -12,7 +12,12 @@ </template> <template #diffMain> <div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden"> - <CustomCalendar :items="formattedItems" :allow-interaction="false" @event-select="eventClick" /> + <CustomCalendar + :items="formattedItems" + :allow-interaction="false" + :small-styling="true" + @event-select="eventClick" + /> </div> </template> </MainTemplate>